From 73a6374de21dd92743558d08beff995d296f3c4b Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Tue, 26 Sep 2023 15:38:50 +0200 Subject: [PATCH] Move test/integration/production to test/production (#55981) Moves `test/integration/production` to `test/production/pages-dir/production` to leverage test isolation, useful for when Turbopack runs for the test. --- .github/workflows/build_and_test.yml | 4 +- azure-pipelines.yml | 2 +- .../integration/production/test/index.test.js | 1336 ----------------- .../dynamic-css/many-imports/with-css-1.js | 0 .../many-imports/with-css-1.module.css | 0 .../dynamic-css/many-imports/with-css-2.js | 0 .../many-imports/with-css-2.module.css | 0 .../dynamic-css/many-imports/with-css-3.js | 0 .../many-imports/with-css-3.module.css | 0 .../many-modules/with-css-2.module.css | 0 .../dynamic-css/many-modules/with-css.js | 0 .../many-modules/with-css.module.css | 0 .../components/dynamic-css/nested/Nested.jsx | 0 .../dynamic-css/nested/with-css-2.module.css | 0 .../components/dynamic-css/nested/with-css.js | 0 .../dynamic-css/nested/with-css.module.css | 0 .../fixture}/components/dynamic-css/no-css.js | 0 .../shared-css-module/with-css-2.js | 0 .../shared-css-module/with-css-2.module.css | 0 .../with-css-shared.module.css | 0 .../dynamic-css/shared-css-module/with-css.js | 0 .../shared-css-module/with-css.module.css | 0 .../components/dynamic-css/with-css.js | 0 .../dynamic-css/with-css.module.css | 0 .../fixture}/components/hello-context.js | 0 .../production/fixture}/components/hello1.js | 0 .../production/fixture}/components/hello2.js | 0 .../fixture}/components/logo/dark.svg | 0 .../fixture}/components/logo/index.js | 0 .../fixture}/components/logo/logo.module.css | 0 .../production/fixture}/components/welcome.js | 0 .../pages-dir/production/fixture}/info.json | 0 .../production/fixture}/next.config.js | 0 .../production/fixture}/pages/about.js | 0 .../production/fixture}/pages/amp-hybrid.js | 0 .../production/fixture}/pages/amp.js | 0 .../production/fixture}/pages/another.js | 0 .../fixture}/pages/api/[post]/index.js | 0 .../production/fixture}/pages/api/hello.js | 0 .../production/fixture}/pages/api/index.js | 0 .../fixture}/pages/api/readfile-dirname.js | 0 .../fixture}/pages/api/readfile-processcwd.js | 0 .../production/fixture}/pages/bad-promise.js | 0 .../production/fixture}/pages/client-error.js | 0 .../production/fixture}/pages/counter.js | 0 .../production/fixture}/pages/css-and-back.js | 0 .../production/fixture}/pages/css-modules.js | 0 .../fixture}/pages/development-logs/index.js | 0 .../production/fixture}/pages/dynamic/css.js | 0 .../fixture}/pages/dynamic/index.js | 0 .../pages/dynamic/many-css-modules.js | 0 .../pages/dynamic/many-dynamic-css.js | 0 .../fixture}/pages/dynamic/nested-css.js | 0 .../fixture}/pages/dynamic/no-chunk.js | 0 .../fixture}/pages/dynamic/no-css.js | 0 .../pages/dynamic/no-ssr-custom-loading.js | 0 .../fixture}/pages/dynamic/no-ssr.js | 0 .../fixture}/pages/dynamic/pagechange1.js | 0 .../fixture}/pages/dynamic/pagechange2.js | 0 .../pages/dynamic/shared-css-module.js | 0 .../fixture}/pages/dynamic/ssr-true.js | 0 .../production/fixture}/pages/dynamic/ssr.js | 0 .../error-in-browser-render-status-code.js | 0 .../fixture}/pages/error-in-browser-render.js | 0 .../fixture}/pages/error-in-ssr-render.js | 0 .../fixture}/pages/external-and-back.js | 0 .../fixture}/pages/finish-response.js | 0 .../fixture}/pages/fully-dynamic.js | 0 .../production/fixture}/pages/fully-static.js | 0 .../production/fixture}/pages/index.js | 0 .../fixture}/pages/invalid-param/[slug].js | 0 .../production/fixture}/pages/mark-in-head.js | 0 .../production/fixture}/pages/next-import.js | 0 .../fixture}/pages/node-browser-polyfills.js | 0 .../production/fixture}/pages/prefetch.js | 0 .../production/fixture}/pages/process-env.js | 0 .../production/fixture}/pages/query.js | 0 .../fixture}/pages/regexp-polyfill.js | 0 .../fixture}/pages/runtime-config.js | 0 .../fixture}/pages/shadowed-page.js | 0 .../production/fixture}/pages/some-amp.js | 0 .../production/fixture}/pages/static-image.js | 0 .../production/fixture}/pages/svg-image.js | 0 .../fixture}/pages/to-nonexistent.js | 0 .../fixture}/pages/to-shadowed-page.js | 0 .../production/fixture}/pages/with-title.js | 0 .../production/fixture}/public/data/data.txt | 0 .../pages-dir/production/fixture}/public/file | 0 .../production/fixture}/public/regexp-test.js | 0 .../fixture}/public/static/legacy.txt | 0 .../production/fixture}/public/vercel.png | Bin .../production/fixture}/public/xss.svg | 0 .../production/fixture}/static/data/item.txt | 0 .../production/fixture}/static/hello.json | 0 .../pages-dir/production/test/dynamic.ts} | 16 +- .../pages-dir/production/test/index.test.ts | 1312 ++++++++++++++++ .../pages-dir/production/test/process-env.ts} | 12 +- .../pages-dir/production/test/security.ts} | 50 +- 98 files changed, 1354 insertions(+), 1378 deletions(-) delete mode 100644 test/integration/production/test/index.test.js rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/many-imports/with-css-1.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/many-imports/with-css-1.module.css (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/many-imports/with-css-2.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/many-imports/with-css-2.module.css (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/many-imports/with-css-3.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/many-imports/with-css-3.module.css (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/many-modules/with-css-2.module.css (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/many-modules/with-css.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/many-modules/with-css.module.css (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/nested/Nested.jsx (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/nested/with-css-2.module.css (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/nested/with-css.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/nested/with-css.module.css (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/no-css.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/shared-css-module/with-css-2.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/shared-css-module/with-css-2.module.css (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/shared-css-module/with-css-shared.module.css (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/shared-css-module/with-css.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/shared-css-module/with-css.module.css (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/with-css.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/dynamic-css/with-css.module.css (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/hello-context.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/hello1.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/hello2.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/logo/dark.svg (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/logo/index.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/logo/logo.module.css (100%) rename test/{integration/production => production/pages-dir/production/fixture}/components/welcome.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/info.json (100%) rename test/{integration/production => production/pages-dir/production/fixture}/next.config.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/about.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/amp-hybrid.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/amp.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/another.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/api/[post]/index.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/api/hello.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/api/index.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/api/readfile-dirname.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/api/readfile-processcwd.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/bad-promise.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/client-error.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/counter.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/css-and-back.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/css-modules.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/development-logs/index.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/dynamic/css.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/dynamic/index.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/dynamic/many-css-modules.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/dynamic/many-dynamic-css.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/dynamic/nested-css.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/dynamic/no-chunk.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/dynamic/no-css.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/dynamic/no-ssr-custom-loading.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/dynamic/no-ssr.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/dynamic/pagechange1.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/dynamic/pagechange2.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/dynamic/shared-css-module.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/dynamic/ssr-true.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/dynamic/ssr.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/error-in-browser-render-status-code.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/error-in-browser-render.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/error-in-ssr-render.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/external-and-back.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/finish-response.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/fully-dynamic.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/fully-static.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/index.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/invalid-param/[slug].js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/mark-in-head.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/next-import.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/node-browser-polyfills.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/prefetch.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/process-env.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/query.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/regexp-polyfill.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/runtime-config.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/shadowed-page.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/some-amp.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/static-image.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/svg-image.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/to-nonexistent.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/to-shadowed-page.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/pages/with-title.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/public/data/data.txt (100%) rename test/{integration/production => production/pages-dir/production/fixture}/public/file (100%) rename test/{integration/production => production/pages-dir/production/fixture}/public/regexp-test.js (100%) rename test/{integration/production => production/pages-dir/production/fixture}/public/static/legacy.txt (100%) rename test/{integration/production => production/pages-dir/production/fixture}/public/vercel.png (100%) rename test/{integration/production => production/pages-dir/production/fixture}/public/xss.svg (100%) rename test/{integration/production => production/pages-dir/production/fixture}/static/data/item.txt (100%) rename test/{integration/production => production/pages-dir/production/fixture}/static/hello.json (100%) rename test/{integration/production/test/dynamic.js => production/pages-dir/production/test/dynamic.ts} (91%) create mode 100644 test/production/pages-dir/production/test/index.test.ts rename test/{integration/production/test/process-env.js => production/pages-dir/production/test/process-env.ts} (85%) rename test/{integration/production/test/security.js => production/pages-dir/production/test/security.ts} (91%) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 62e123c221..aab3fe4e03 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -172,7 +172,7 @@ jobs: uses: ./.github/workflows/build_reusable.yml with: skipForDocsOnly: 'yes' - afterBuild: rustup target add wasm32-unknown-unknown && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh && node ./scripts/normalize-version-bump.js && turbo run build-wasm -- --target nodejs --features tracing/release_max_level_info && git checkout . && mv packages/next-swc/crates/wasm/pkg packages/next-swc/crates/wasm/pkg-nodejs && node ./scripts/setup-wasm.mjs && NEXT_TEST_MODE=start TEST_WASM=true node run-tests.js test/integration/production/test/index.test.js test/e2e/streaming-ssr/index.test.ts + afterBuild: rustup target add wasm32-unknown-unknown && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh && node ./scripts/normalize-version-bump.js && turbo run build-wasm -- --target nodejs --features tracing/release_max_level_info && git checkout . && mv packages/next-swc/crates/wasm/pkg packages/next-swc/crates/wasm/pkg-nodejs && node ./scripts/setup-wasm.mjs && NEXT_TEST_MODE=start TEST_WASM=true node run-tests.js test/production/pages-dir/production/test/index.test.ts test/e2e/streaming-ssr/index.test.ts secrets: inherit test-unit: @@ -238,7 +238,7 @@ jobs: uses: ./.github/workflows/build_reusable.yml with: skipForDocsOnly: 'yes' - afterBuild: pnpm playwright install && BROWSER_NAME=firefox node run-tests.js test/integration/production/test/index.test.js && BROWSER_NAME=safari NEXT_TEST_MODE=start node run-tests.js -c 1 test/integration/production/test/index.test.js test/e2e/basepath.test.ts && BROWSER_NAME=safari DEVICE_NAME='iPhone XR' node run-tests.js -c 1 test/production/prerender-prefetch/index.test.ts + afterBuild: pnpm playwright install && BROWSER_NAME=firefox node run-tests.js test/production/pages-dir/production/test/index.test.ts && BROWSER_NAME=safari NEXT_TEST_MODE=start node run-tests.js -c 1 test/production/pages-dir/production/test/index.test.ts test/e2e/basepath.test.ts && BROWSER_NAME=safari DEVICE_NAME='iPhone XR' node run-tests.js -c 1 test/production/prerender-prefetch/index.test.ts secrets: inherit report-test-results: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index decaec840f..4c5c09be61 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -70,7 +70,7 @@ stages: condition: eq(variables['isDocsOnly'], 'No') - script: | - node run-tests.js -c 1 test/integration/production/test/index.test.js test/integration/css-client-nav/test/index.test.js test/integration/rewrites-has-condition/test/index.test.js + node run-tests.js -c 1 test/production/pages-dir/production/test/index.test.ts test/integration/css-client-nav/test/index.test.js test/integration/rewrites-has-condition/test/index.test.js condition: eq(variables['isDocsOnly'], 'No') displayName: 'Run tests' diff --git a/test/integration/production/test/index.test.js b/test/integration/production/test/index.test.js deleted file mode 100644 index ed861434b6..0000000000 --- a/test/integration/production/test/index.test.js +++ /dev/null @@ -1,1336 +0,0 @@ -/* eslint-env jest */ -/* global browserName */ -import cheerio from 'cheerio' -import fs, { existsSync } from 'fs-extra' -import globOriginal from 'glob' -import { - renderViaHTTP, - waitFor, - getPageFileFromPagesManifest, - check, - nextBuild, - nextStart, - findPort, - killApp, - fetchViaHTTP, -} from 'next-test-utils' -import webdriver from 'next-webdriver' -import { - BUILD_MANIFEST, - PAGES_MANIFEST, - REACT_LOADABLE_MANIFEST, -} from 'next/constants' -import { recursiveReadDir } from 'next/dist/lib/recursive-readdir' -import { join, sep } from 'path' -import dynamicImportTests from './dynamic' -import processEnv from './process-env' -import security from './security' -import { promisify } from 'util' -import { error } from 'console' - -const glob = promisify(globOriginal) - -const appDir = join(__dirname, '../') -let appPort -let app - -const context = {} - -if (process.env.TEST_WASM) { - jest.setTimeout(240 * 1000) -} - -describe('Production Usage', () => { - let output = '' - beforeAll(async () => { - let opts = { - stderr: true, - stdout: true, - } - if (process.env.TEST_WASM) { - opts.env = { - NODE_OPTIONS: '--no-addons', - } - } - const result = await nextBuild(appDir, undefined, opts) - - appPort = await findPort() - context.appPort = appPort - app = await nextStart(appDir, appPort, { cwd: appDir }) - output = (result.stderr || '') + (result.stdout || '') - - if (result.code !== 0) { - error(output) - throw new Error(`Failed to build, exited with code ${result.code}`) - } else { - // Note: jest captures calls to console and only emits when there's assertion fails, - // so this won't log anything for normal test execution path. - console.log(output) - } - }) - afterAll(async () => { - await killApp(app) - }) - - it('should navigate through history after query update', async () => { - const browser = await webdriver(appPort, '/') - await browser.eval('window.next.router.push("/about?a=b")') - await browser.waitForElementByCss('.about-page') - await browser.waitForCondition(`!!window.next.router.isReady`) - - await browser.refresh() - await browser.waitForCondition(`!!window.next.router.isReady`) - await browser.back() - await browser.waitForElementByCss('.index-page') - await browser.forward() - await browser.waitForElementByCss('.about-page') - await browser.back() - await browser.waitForElementByCss('.index-page') - await browser.refresh() - await browser.waitForCondition(`!!window.next.router.isReady`) - await browser.forward() - await browser.waitForElementByCss('.about-page') - }) - - if (process.env.BROWSER_NAME !== 'safari') { - it.each([ - { hash: '#hello?' }, - { hash: '#?' }, - { hash: '##' }, - { hash: '##?' }, - { hash: '##hello?' }, - { hash: '##hello' }, - { hash: '#hello?world' }, - { search: '?hello=world', hash: '#a', query: { hello: 'world' } }, - { search: '?hello', hash: '#a', query: { hello: '' } }, - { search: '?hello=', hash: '#a', query: { hello: '' } }, - ])( - 'should handle query/hash correctly during query updating $hash $search', - async ({ hash, search, query }) => { - const browser = await webdriver( - appPort, - `/${search || ''}${hash || ''}` - ) - - await check( - () => - browser.eval('window.next.router.isReady ? "ready" : "not ready"'), - 'ready' - ) - expect(await browser.eval('window.location.pathname')).toBe('/') - expect(await browser.eval('window.location.hash')).toBe(hash || '') - expect(await browser.eval('window.location.search')).toBe(search || '') - expect(await browser.eval('next.router.pathname')).toBe('/') - expect( - JSON.parse(await browser.eval('JSON.stringify(next.router.query)')) - ).toEqual(query || {}) - } - ) - } - - it('should not show target deprecation warning', () => { - expect(output).not.toContain( - 'The `target` config is deprecated and will be removed in a future version' - ) - }) - - it('should respond with 405 for POST to static page', async () => { - const res = await fetchViaHTTP(appPort, '/', undefined, { - method: 'POST', - }) - expect(res.status).toBe(405) - expect(await res.text()).toContain('Method Not Allowed') - }) - - it('should contain generated page count in output', async () => { - const pageCount = 40 - expect(output).toContain(`Generating static pages (0/${pageCount})`) - expect(output).toContain( - `Generating static pages (${pageCount}/${pageCount})` - ) - // we should only have 4 segments and the initial message logged out - expect(output.match(/Generating static pages/g).length).toBe(5) - }) - - it('should output traces', async () => { - const serverTrace = await fs.readJSON( - join(appDir, '.next/next-server.js.nft.json') - ) - - expect(serverTrace.version).toBe(1) - expect( - serverTrace.files.some((file) => - file.includes('next/dist/server/send-payload/index.js') - ) - ).toBe(true) - expect( - serverTrace.files.some((file) => - file.includes('next/dist/server/lib/route-resolver.js') - ) - ).toBe(false) - const repoRoot = join(__dirname, '../../../../') - expect( - serverTrace.files.some((file) => { - const fullPath = join(__dirname, '../.next', file) - if (!fullPath.startsWith(repoRoot)) { - console.error(`Found next-server trace file outside repo root`, { - repoRoot, - fullPath, - file, - }) - return true - } - return false - }) - ).toBe(false) - expect( - serverTrace.files.some((file) => - file.includes('next/dist/shared/lib/page-path/normalize-page-path.js') - ) - ).toBe(true) - expect( - serverTrace.files.some((file) => - file.includes('next/dist/server/render.js') - ) - ).toBe(true) - expect( - serverTrace.files.some((file) => - file.includes('next/dist/server/load-components.js') - ) - ).toBe(true) - - if (process.platform !== 'win32') { - expect( - serverTrace.files.some((file) => - file.includes('next/dist/compiled/webpack/bundle5.js') - ) - ).toBe(false) - expect( - serverTrace.files.some((file) => file.includes('node_modules/sharp')) - ).toBe(false) - } - - const checks = [ - { - page: '/_app', - tests: [ - /webpack-runtime\.js/, - /node_modules\/react\/index\.js/, - /node_modules\/react\/package\.json/, - /node_modules\/react\/cjs\/react\.production\.min\.js/, - ], - notTests: [/\0/, /\?/, /!/], - }, - { - page: '/client-error', - tests: [ - /webpack-runtime\.js/, - /chunks\/.*?\.js/, - /node_modules\/react\/index\.js/, - /node_modules\/react\/package\.json/, - /node_modules\/react\/cjs\/react\.production\.min\.js/, - /node_modules\/next/, - /next\/link\.js/, - /next\/error\.js/, - ], - notTests: [/\0/, /\?/, /!/], - }, - { - page: '/dynamic', - tests: [ - /webpack-runtime\.js/, - /chunks\/.*?\.js/, - /node_modules\/react\/index\.js/, - /node_modules\/react\/package\.json/, - /node_modules\/react\/cjs\/react\.production\.min\.js/, - /node_modules\/next/, - /next\/link\.js/, - ], - notTests: [/\0/, /\?/, /!/], - }, - { - page: '/index', - tests: [ - /webpack-runtime\.js/, - /chunks\/.*?\.js/, - /node_modules\/react\/index\.js/, - /node_modules\/react\/package\.json/, - /node_modules\/react\/cjs\/react\.production\.min\.js/, - /node_modules\/next/, - /next\/link\.js/, - /node_modules\/nanoid\/index\.js/, - /node_modules\/nanoid\/url-alphabet\/index\.js/, - /node_modules\/es5-ext\/array\/#\/clear\.js/, - ], - notTests: [ - /next\/dist\/pages\/_error\.js/, - /next\/error\.js/, - /\0/, - /\?/, - /!/, - ], - }, - { - page: '/counter', - tests: [ - /webpack-runtime\.js/, - /chunks\/.*?\.js/, - /node_modules\/react\/index\.js/, - /node_modules\/react\/package\.json/, - /node_modules\/react\/cjs\/react\.production\.min\.js/, - /node_modules\/react\/cjs\/react\.development\.js/, - /node_modules\/next/, - /next\/router\.js/, - ], - notTests: [/\0/, /\?/, /!/], - }, - { - page: '/next-import', - tests: [ - /webpack-runtime\.js/, - /chunks\/.*?\.js/, - /node_modules\/react\/index\.js/, - /node_modules\/react\/package\.json/, - /node_modules\/react\/cjs\/react\.production\.min\.js/, - /node_modules\/next/, - /next\/link\.js/, - ], - notTests: [ - /next\/dist\/server\/next\.js/, - /next\/dist\/bin/, - /\0/, - /\?/, - /!/, - ], - }, - { - page: '/api', - tests: [/webpack-runtime\.js/, /\/logo\.module\.css/], - notTests: [ - /next\/dist\/server\/next\.js/, - /next\/dist\/bin/, - /\0/, - /\?/, - /!/, - ], - }, - { - page: '/api/readfile-dirname', - tests: [/webpack-api-runtime\.js/, /static\/data\/item\.txt/], - notTests: [ - /next\/dist\/server\/next\.js/, - /next\/dist\/bin/, - /\0/, - /\?/, - /!/, - ], - }, - { - page: '/api/readfile-processcwd', - tests: [/webpack-api-runtime\.js/, /static\/data\/item\.txt/], - notTests: [ - /next\/dist\/server\/next\.js/, - /next\/dist\/bin/, - /\0/, - /\?/, - /!/, - ], - }, - ] - - for (const check of checks) { - const contents = await fs.readFile( - join(appDir, '.next/server/pages/', check.page + '.js.nft.json'), - 'utf8' - ) - const { version, files } = JSON.parse(contents) - expect(version).toBe(1) - expect([...new Set(files)].length).toBe(files.length) - - expect( - check.tests.every((item) => { - if (files.some((file) => item.test(file))) { - return true - } - console.error( - `Failed to find ${item} for page ${check.page} in`, - files - ) - return false - }) - ).toBe(true) - - if (sep === '/') { - expect( - check.notTests.some((item) => { - if (files.some((file) => item.test(file))) { - console.error(`Found unexpected ${item} in`, files) - return true - } - return false - }) - ).toBe(false) - } - } - }) - - it('should not contain currentScript usage for publicPath', async () => { - const globResult = await glob('webpack-*.js', { - cwd: join(appDir, '.next/static/chunks'), - }) - - if (!globResult || globResult.length !== 1) { - throw new Error('could not find webpack-hash.js chunk') - } - - const content = await fs.readFile( - join(appDir, '.next/static/chunks', globResult[0]), - 'utf8' - ) - - expect(content).not.toContain('.currentScript') - }) - - it('should not contain amp, rsc APIs in main chunk', async () => { - const globResult = await glob('main-*.js', { - cwd: join(appDir, '.next/static/chunks'), - }) - - if (!globResult || globResult.length !== 1) { - throw new Error('could not find main js chunk') - } - - const content = await fs.readFile( - join(appDir, '.next/static/chunks', globResult[0]), - 'utf8' - ) - - expect(content).not.toContain('useAmp') - expect(content).not.toContain('useRefreshRoot') - }) - - describe('With basic usage', () => { - it('should render the page', async () => { - const html = await renderViaHTTP(appPort, '/') - expect(html).toMatch(/Hello World/) - }) - - if (browserName === 'internet explorer') { - it('should handle bad Promise polyfill', async () => { - const browser = await webdriver(appPort, '/bad-promise') - expect(await browser.eval('window.didRender')).toBe(true) - }) - - it('should polyfill RegExp successfully', async () => { - const browser = await webdriver(appPort, '/regexp-polyfill') - expect(await browser.eval('window.didRender')).toBe(true) - // wait a second for the script to be loaded - await waitFor(1000) - - expect(await browser.eval('window.isSticky')).toBe(true) - expect(await browser.eval('window.isMatch1')).toBe(true) - expect(await browser.eval('window.isMatch2')).toBe(false) - }) - } - - it('should polyfill Node.js modules', async () => { - const browser = await webdriver(appPort, '/node-browser-polyfills') - await browser.waitForCondition('window.didRender') - - const data = await browser - .waitForElementByCss('#node-browser-polyfills') - .text() - const parsedData = JSON.parse(data) - - expect(parsedData.vm).toBe(105) - expect(parsedData.hash).toBe( - 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' - ) - expect(parsedData.path).toBe('/hello/world/test.txt') - expect(parsedData.buffer).toBe('hello world') - expect(parsedData.stream).toBe(true) - }) - - it('should allow etag header support', async () => { - const url = `http://localhost:${appPort}` - const etag = (await fetchViaHTTP(url, '/')).headers.get('ETag') - - const headers = { 'If-None-Match': etag } - const res2 = await fetchViaHTTP(url, '/', undefined, { headers }) - expect(res2.status).toBe(304) - }) - - it('should allow etag header support with getStaticProps', async () => { - const url = `http://localhost:${appPort}` - const etag = (await fetchViaHTTP(url, '/fully-static')).headers.get( - 'ETag' - ) - - const headers = { 'If-None-Match': etag } - const res2 = await fetchViaHTTP(url, '/fully-static', undefined, { - headers, - }) - expect(res2.status).toBe(304) - }) - - // TODO: should we generate weak etags for streaming getServerSideProps? - // this is currently not expected to work with react-18 - it.skip('should allow etag header support with getServerSideProps', async () => { - const url = `http://localhost:${appPort}` - const etag = (await fetchViaHTTP(url, '/fully-dynamic')).headers.get( - 'ETag' - ) - - const headers = { 'If-None-Match': etag } - const res2 = await fetchViaHTTP(url, '/fully-dynamic', undefined, { - headers, - }) - expect(res2.status).toBe(304) - }) - - it('should have X-Powered-By header support', async () => { - const url = `http://localhost:${appPort}` - const header = (await fetchViaHTTP(url, '/')).headers.get('X-Powered-By') - - expect(header).toBe('Next.js') - }) - - it('should render 404 for routes that do not exist', async () => { - const url = `http://localhost:${appPort}` - const res = await fetchViaHTTP(url, '/abcdefghijklmno') - const text = await res.text() - const $html = cheerio.load(text) - expect($html('html').text()).toMatch(/404/) - expect(text).toMatch(/"statusCode":404/) - expect(res.status).toBe(404) - }) - - it('should render 404 for /_next/static route', async () => { - const html = await renderViaHTTP(appPort, '/_next/static') - expect(html).toMatch(/This page could not be found/) - }) - - it('should render 200 for POST on page', async () => { - const res = await fetchViaHTTP( - `http://localhost:${appPort}`, - '/fully-dynamic', - undefined, - { - method: 'POST', - } - ) - expect(res.status).toBe(200) - }) - - it('should render 404 for POST on missing page', async () => { - const res = await fetchViaHTTP( - `http://localhost:${appPort}`, - '/fake-page', - undefined, - { - method: 'POST', - } - ) - expect(res.status).toBe(404) - }) - - it('should render 404 for _next routes that do not exist', async () => { - const url = `http://localhost:${appPort}` - const res = await fetchViaHTTP(url, '/_next/abcdef') - expect(res.status).toBe(404) - }) - - it('should render 404 even if the HTTP method is not GET or HEAD', async () => { - const url = `http://localhost:${appPort}` - const methods = ['POST', 'PUT', 'DELETE'] - for (const method of methods) { - const res = await fetchViaHTTP(url, '/_next/abcdef', undefined, { - method, - }) - expect(res.status).toBe(404) - } - }) - - it('should render 404 for dotfiles in /static', async () => { - const url = `http://localhost:${appPort}` - const res = await fetchViaHTTP(url, '/static/.env') - expect(res.status).toBe(404) - }) - - it('should return 405 method on static then GET and HEAD', async () => { - const res = await fetchViaHTTP( - `http://localhost:${appPort}`, - '/static/data/item.txt', - undefined, - { - method: 'POST', - } - ) - expect(res.headers.get('allow').includes('GET')).toBe(true) - expect(res.status).toBe(405) - }) - - it('should return 412 on static file when If-Unmodified-Since is provided and file is modified', async () => { - const buildManifest = require(join( - __dirname, - '../.next/build-manifest.json' - )) - - const files = buildManifest.pages['/'] - - for (const file of files) { - const res = await fetchViaHTTP( - `http://localhost:${appPort}`, - `/_next/${file}`, - undefined, - { - method: 'GET', - headers: { 'if-unmodified-since': 'Fri, 12 Jul 2019 20:00:13 GMT' }, - } - ) - expect(res.status).toBe(412) - } - }) - - it('should return 200 on static file if If-Unmodified-Since is invalid date', async () => { - const buildManifest = require(join( - __dirname, - '../.next/build-manifest.json' - )) - - const files = buildManifest.pages['/'] - - for (const file of files) { - const res = await fetchViaHTTP( - `http://localhost:${appPort}`, - `/_next/${file}`, - undefined, - { - method: 'GET', - headers: { 'if-unmodified-since': 'nextjs' }, - } - ) - expect(res.status).toBe(200) - } - }) - - it('should set Content-Length header', async () => { - const url = `http://localhost:${appPort}` - const res = await fetchViaHTTP(url, '/') - expect(res.headers.get('Content-Length')).toBeDefined() - }) - - it('should set Cache-Control header', async () => { - const buildManifest = require(join('../.next', BUILD_MANIFEST)) - const reactLoadableManifest = require(join( - '../.next', - REACT_LOADABLE_MANIFEST - )) - const url = `http://localhost:${appPort}` - - const resources = new Set() - - const manifestKey = Object.keys(reactLoadableManifest).find((item) => { - return item - .replace(/\\/g, '/') - .endsWith('dynamic/css.js -> ../../components/dynamic-css/with-css') - }) - - // test dynamic chunk - reactLoadableManifest[manifestKey].files.forEach((f) => { - resources.add('/' + f) - }) - - // test main.js runtime etc - for (const item of buildManifest.pages['/']) { - resources.add('/' + item) - } - - const cssStaticAssets = await recursiveReadDir( - join(__dirname, '..', '.next', 'static'), - { pathnameFilter: (f) => /\.css$/.test(f) } - ) - expect(cssStaticAssets.length).toBeGreaterThanOrEqual(1) - expect(cssStaticAssets[0]).toMatch(/[\\/]css[\\/]/) - const mediaStaticAssets = await recursiveReadDir( - join(__dirname, '..', '.next', 'static'), - { pathnameFilter: (f) => /\.svg$/.test(f) } - ) - expect(mediaStaticAssets.length).toBeGreaterThanOrEqual(1) - expect(mediaStaticAssets[0]).toMatch(/[\\/]media[\\/]/) - ;[...cssStaticAssets, ...mediaStaticAssets].forEach((asset) => { - resources.add(`/static${asset.replace(/\\+/g, '/')}`) - }) - - const responses = await Promise.all( - [...resources].map((resource) => - fetchViaHTTP(url, join('/_next', resource)) - ) - ) - - responses.forEach((res) => { - try { - expect(res.headers.get('Cache-Control')).toBe( - 'public, max-age=31536000, immutable' - ) - } catch (err) { - err.message = res.url + ' ' + err.message - throw err - } - }) - }) - - it('should set correct Cache-Control header for static 404s', async () => { - // this is to fix where 404 headers are set to 'public, max-age=31536000, immutable' - const res = await fetchViaHTTP( - `http://localhost:${appPort}`, - `/_next//static/common/bad-static.js` - ) - - expect(res.status).toBe(404) - expect(res.headers.get('Cache-Control')).toBe( - 'no-cache, no-store, max-age=0, must-revalidate' - ) - }) - - it('should block special pages', async () => { - const urls = ['/_document', '/_app'] - for (const url of urls) { - const html = await renderViaHTTP(appPort, url) - expect(html).toMatch(/404/) - } - }) - - it('should not contain customServer in NEXT_DATA', async () => { - const html = await renderViaHTTP(appPort, '/') - const $ = cheerio.load(html) - expect('customServer' in JSON.parse($('#__NEXT_DATA__').text())).toBe( - false - ) - }) - }) - - describe('API routes', () => { - it('should work with pages/api/index.js', async () => { - const url = `http://localhost:${appPort}` - const res = await fetchViaHTTP(url, `/api`) - const body = await res.text() - expect(body).toEqual('API index works') - }) - - it('should work with pages/api/hello.js', async () => { - const url = `http://localhost:${appPort}` - const res = await fetchViaHTTP(url, `/api/hello`) - const body = await res.text() - expect(body).toEqual('API hello works') - }) - - // Today, `__dirname` usage fails because Next.js moves the source file - // to .next/server/pages/api but it doesn't move the asset file. - // In the future, it would be nice to make `__dirname` work too. - it('does not work with pages/api/readfile-dirname.js', async () => { - const url = `http://localhost:${appPort}` - const res = await fetchViaHTTP(url, `/api/readfile-dirname`) - expect(res.status).toBe(500) - }) - - it('should work with pages/api/readfile-processcwd.js', async () => { - const url = `http://localhost:${appPort}` - const res = await fetchViaHTTP(url, `/api/readfile-processcwd`) - const body = await res.text() - expect(body).toBe('item') - }) - - it('should work with dynamic params and search string', async () => { - const url = `http://localhost:${appPort}` - const res = await fetchViaHTTP(url, `/api/post-1?val=1`) - const body = await res.json() - - expect(body).toEqual({ val: '1', post: 'post-1' }) - }) - }) - - describe('With navigation', () => { - it('should navigate via client side', async () => { - const browser = await webdriver(appPort, '/') - const text = await browser - .elementByCss('a') - .click() - .waitForElementByCss('.about-page') - .elementByCss('.about-page') - .text() - - expect(text).toBe('About Page') - await browser.close() - }) - - it('should navigate to nested index via client side', async () => { - const browser = await webdriver(appPort, '/another') - await browser.eval('window.beforeNav = 1') - - const text = await browser - .elementByCss('a') - .click() - .waitForElementByCss('.index-page') - .elementByCss('p') - .text() - - expect(text).toBe('Hello World') - expect(await browser.eval('window.beforeNav')).toBe(1) - await browser.close() - }) - - it('should reload page successfully (on bad link)', async () => { - const browser = await webdriver(appPort, '/to-nonexistent') - await browser.eval(function setup() { - window.__DATA_BE_GONE = 'true' - }) - await browser.waitForElementByCss('#to-nonexistent-page') - await browser.click('#to-nonexistent-page') - await browser.waitForElementByCss('.about-page') - - const oldData = await browser.eval(`window.__DATA_BE_GONE`) - expect(oldData).toBeFalsy() - }) - - it('should reload page successfully (on bad data fetch)', async () => { - const browser = await webdriver(appPort, '/to-shadowed-page') - await browser.eval(function setup() { - window.__DATA_BE_GONE = 'true' - }) - await browser.waitForElementByCss('#to-shadowed-page').click() - await browser.waitForElementByCss('.about-page') - - const oldData = await browser.eval(`window.__DATA_BE_GONE`) - expect(oldData).toBeFalsy() - }) - }) - - it('should navigate to external site and back', async () => { - const browser = await webdriver(appPort, '/external-and-back') - const initialText = await browser.elementByCss('p').text() - expect(initialText).toBe('server') - - await browser - .elementByCss('a') - .click() - .waitForElementByCss('input') - .back() - .waitForElementByCss('p') - - await waitFor(1000) - const newText = await browser.elementByCss('p').text() - expect(newText).toBe('server') - }) - - it('should navigate to page with CSS and back', async () => { - const browser = await webdriver(appPort, '/css-and-back') - const initialText = await browser.elementByCss('p').text() - expect(initialText).toBe('server') - - await browser - .elementByCss('a') - .click() - .waitForElementByCss('input') - .back() - .waitForElementByCss('p') - - await waitFor(1000) - const newText = await browser.elementByCss('p').text() - expect(newText).toBe('client') - }) - - it('should navigate to external site and back (with query)', async () => { - const browser = await webdriver(appPort, '/external-and-back?hello=world') - const initialText = await browser.elementByCss('p').text() - expect(initialText).toBe('server') - - await browser - .elementByCss('a') - .click() - .waitForElementByCss('input') - .back() - .waitForElementByCss('p') - - await waitFor(1000) - const newText = await browser.elementByCss('p').text() - expect(newText).toBe('server') - }) - - it('should change query correctly', async () => { - const browser = await webdriver(appPort, '/query?id=0') - let id = await browser.elementByCss('#q0').text() - expect(id).toBe('0') - - await browser.elementByCss('#first').click().waitForElementByCss('#q1') - - id = await browser.elementByCss('#q1').text() - expect(id).toBe('1') - - await browser.elementByCss('#second').click().waitForElementByCss('#q2') - - id = await browser.elementByCss('#q2').text() - expect(id).toBe('2') - }) - - describe('Runtime errors', () => { - it('should render a server side error on the client side', async () => { - const browser = await webdriver(appPort, '/error-in-ssr-render') - await waitFor(2000) - const text = await browser.elementByCss('body').text() - // this makes sure we don't leak the actual error to the client side in production - expect(text).toMatch(/Internal Server Error\./) - const headingText = await browser.elementByCss('h1').text() - // This makes sure we render statusCode on the client side correctly - expect(headingText).toBe('500') - await browser.close() - }) - - it('should render a client side component error', async () => { - const browser = await webdriver(appPort, '/error-in-browser-render') - await waitFor(2000) - const text = await browser.elementByCss('body').text() - expect(text).toMatch( - /Application error: a client-side exception has occurred/ - ) - await browser.close() - }) - - it('should call getInitialProps on _error page during a client side component error', async () => { - const browser = await webdriver( - appPort, - '/error-in-browser-render-status-code' - ) - await waitFor(2000) - const text = await browser.elementByCss('body').text() - expect(text).toMatch(/This page could not be found\./) - await browser.close() - }) - }) - - describe('Misc', () => { - it('should handle already finished responses', async () => { - const html = await renderViaHTTP(appPort, '/finish-response') - expect(html).toBe('hi') - }) - - it('should allow to access /static/ and /_next/', async () => { - // This is a test case which prevent the following issue happening again. - // See: https://github.com/vercel/next.js/issues/2617 - await renderViaHTTP(appPort, '/_next/') - await renderViaHTTP(appPort, '/static/') - const data = await renderViaHTTP(appPort, '/static/data/item.txt') - expect(data).toBe('item') - }) - - it('Should allow access to public files', async () => { - const data = await renderViaHTTP(appPort, '/data/data.txt') - const file = await renderViaHTTP(appPort, '/file') - const legacy = await renderViaHTTP(appPort, '/static/legacy.txt') - expect(data).toBe('data') - expect(file).toBe('test') - expect(legacy).toMatch(`new static folder`) - }) - - // TODO: do we want to normalize this for firefox? It seems in - // the latest version of firefox the window state is not reset - // when navigating back from a hard navigation. This might be - // a bug as other browsers do not behave this way. - if (browserName !== 'firefox') { - it('should reload the page on page script error', async () => { - const browser = await webdriver(appPort, '/counter') - const counter = await browser - .elementByCss('#increase') - .click() - .click() - .elementByCss('#counter') - .text() - expect(counter).toBe('Counter: 2') - - // When we go to the 404 page, it'll do a hard reload. - // So, it's possible for the front proxy to load a page from another zone. - // Since the page is reloaded, when we go back to the counter page again, - // previous counter value should be gone. - const counterAfter404Page = await browser - .elementByCss('#no-such-page') - .click() - .waitForElementByCss('h1') - .back() - .waitForElementByCss('#counter-page') - .elementByCss('#counter') - .text() - expect(counterAfter404Page).toBe('Counter: 0') - - await browser.close() - }) - } - - it('should have default runtime values when not defined', async () => { - const html = await renderViaHTTP(appPort, '/runtime-config') - expect(html).toMatch(/found public config/) - expect(html).toMatch(/found server config/) - }) - - it('should not have runtimeConfig in __NEXT_DATA__', async () => { - const html = await renderViaHTTP(appPort, '/runtime-config') - const $ = cheerio.load(html) - const script = $('#__NEXT_DATA__').html() - expect(script).not.toMatch(/runtimeConfig/) - }) - - it('should add autoExport for auto pre-rendered pages', async () => { - for (const page of ['/', '/about']) { - const html = await renderViaHTTP(appPort, page) - const $ = cheerio.load(html) - const data = JSON.parse($('#__NEXT_DATA__').html()) - expect(data.autoExport).toBe(true) - } - }) - - it('should not add autoExport for non pre-rendered pages', async () => { - for (const page of ['/query']) { - const html = await renderViaHTTP(appPort, page) - const $ = cheerio.load(html) - const data = JSON.parse($('#__NEXT_DATA__').html()) - expect(!!data.autoExport).toBe(false) - } - }) - - it('should add prefetch tags when Link prefetch prop is used', async () => { - const browser = await webdriver(appPort, '/prefetch') - - if (browserName === 'internet explorer') { - // IntersectionObserver isn't present so we need to trigger manually - await waitFor(1000) - await browser.eval(`(function() { - window.next.router.prefetch('/') - window.next.router.prefetch('/process-env') - window.next.router.prefetch('/counter') - window.next.router.prefetch('/about') - })()`) - } - - await waitFor(2000) - - if (browserName === 'safari') { - const elements = await browser.elementsByCss('link[rel=preload]') - // optimized preloading uses defer instead of preloading and prefetches - // aren't generated client-side since safari does not support prefetch - expect(elements.length).toBe(0) - } else { - const elements = await browser.elementsByCss('link[rel=prefetch]') - expect(elements.length).toBe(4) - - for (const element of elements) { - const rel = await element.getAttribute('rel') - const as = await element.getAttribute('as') - expect(rel).toBe('prefetch') - expect(as).toBe('script') - } - } - await browser.close() - }) - - // This is a workaround to fix https://github.com/vercel/next.js/issues/5860 - // TODO: remove this workaround when https://bugs.webkit.org/show_bug.cgi?id=187726 is fixed. - it('It does not add a timestamp to link tags with prefetch attribute', async () => { - const browser = await webdriver(appPort, '/prefetch') - const links = await browser.elementsByCss('link[rel=prefetch]') - - for (const element of links) { - const href = await element.getAttribute('href') - expect(href).not.toMatch(/\?ts=/) - } - const scripts = await browser.elementsByCss('script[src]') - - for (const element of scripts) { - const src = await element.getAttribute('src') - expect(src).not.toMatch(/\?ts=/) - } - await browser.close() - }) - - if (browserName === 'chrome') { - it('should reload the page on page script error with prefetch', async () => { - const browser = await webdriver(appPort, '/counter') - if (global.browserName !== 'chrome') return - const counter = await browser - .elementByCss('#increase') - .click() - .click() - .elementByCss('#counter') - .text() - expect(counter).toBe('Counter: 2') - - // Let the browser to prefetch the page and error it on the console. - await waitFor(3000) - - // When we go to the 404 page, it'll do a hard reload. - // So, it's possible for the front proxy to load a page from another zone. - // Since the page is reloaded, when we go back to the counter page again, - // previous counter value should be gone. - const counterAfter404Page = await browser - .elementByCss('#no-such-page-prefetch') - .click() - .waitForElementByCss('h1') - .back() - .waitForElementByCss('#counter-page') - .elementByCss('#counter') - .text() - expect(counterAfter404Page).toBe('Counter: 0') - - await browser.close() - }) - } - }) - - it('should not expose the compiled page file in development', async () => { - const url = `http://localhost:${appPort}` - await fetchViaHTTP(`${url}`, `/stateless`) // make sure the stateless page is built - const clientSideJsRes = await fetchViaHTTP( - `${url}`, - '/_next/development/static/development/pages/stateless.js' - ) - expect(clientSideJsRes.status).toBe(404) - const clientSideJsBody = await clientSideJsRes.text() - expect(clientSideJsBody).toMatch(/404/) - - const serverSideJsRes = await fetchViaHTTP( - `${url}`, - '/_next/development/server/static/development/pages/stateless.js' - ) - expect(serverSideJsRes.status).toBe(404) - const serverSideJsBody = await serverSideJsRes.text() - expect(serverSideJsBody).toMatch(/404/) - }) - - it('should not put backslashes in pages-manifest.json', () => { - // Whatever platform you build on, pages-manifest.json should use forward slash (/) - // See: https://github.com/vercel/next.js/issues/4920 - const pagesManifest = require(join('..', '.next', 'server', PAGES_MANIFEST)) - - for (let key of Object.keys(pagesManifest)) { - expect(key).not.toMatch(/\\/) - expect(pagesManifest[key]).not.toMatch(/\\/) - } - }) - - it('should handle failed param decoding', async () => { - const html = await renderViaHTTP(appPort, '/invalid-param/%DE~%C7%1fY/') - expect(html).toMatch(/400/) - expect(html).toMatch(/Bad Request/) - }) - - it('should replace static pages with HTML files', async () => { - const pages = ['/about', '/another', '/counter', '/dynamic', '/prefetch'] - for (const page of pages) { - const file = getPageFileFromPagesManifest(appDir, page) - - expect(file.endsWith('.html')).toBe(true) - } - }) - - it('should not replace non-static pages with HTML files', async () => { - const pages = ['/api', '/external-and-back', '/finish-response'] - - for (const page of pages) { - const file = getPageFileFromPagesManifest(appDir, page) - - expect(file.endsWith('.js')).toBe(true) - } - }) - - it('should handle AMP correctly in IE', async () => { - const browser = await webdriver(appPort, '/some-amp') - const text = await browser.elementByCss('p').text() - expect(text).toBe('Not AMP') - }) - - it('should warn when prefetch is true', async () => { - if (global.browserName !== 'chrome') return - let browser - try { - browser = await webdriver(appPort, '/development-logs') - const browserLogs = await browser.log('browser') - let found = false - browserLogs.forEach((log) => { - if (log.message.includes('Next.js auto-prefetches automatically')) { - found = true - } - }) - expect(found).toBe(false) - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should not emit stats', async () => { - expect(existsSync(join(appDir, '.next', 'next-stats.json'))).toBe(false) - }) - - it('should contain the Next.js version in window export', async () => { - let browser - try { - browser = await webdriver(appPort, '/about') - const version = await browser.eval('window.next.version') - expect(version).toBeTruthy() - expect(version).toBe(require('next/package.json').version) - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should clear all core performance marks', async () => { - let browser - try { - browser = await webdriver(appPort, '/fully-dynamic') - - const currentPerfMarks = await browser.eval( - `window.performance.getEntriesByType('mark')` - ) - const allPerfMarks = [ - 'beforeRender', - 'afterHydrate', - 'afterRender', - 'routeChange', - ] - - allPerfMarks.forEach((name) => - expect(currentPerfMarks).not.toContainEqual( - expect.objectContaining({ name }) - ) - ) - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should not clear custom performance marks', async () => { - let browser - try { - browser = await webdriver(appPort, '/mark-in-head') - - const customMarkFound = await browser.eval( - `window.performance.getEntriesByType('mark').filter(function(e) { - return e.name === 'custom-mark' - }).length === 1` - ) - expect(customMarkFound).toBe(true) - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should have defer on all script tags', async () => { - const html = await renderViaHTTP(appPort, '/') - const $ = cheerio.load(html) - let missing = false - - for (const script of $('script').toArray()) { - // application/json doesn't need async - if ( - script.attribs.type === 'application/json' || - script.attribs.src.includes('polyfills') - ) { - continue - } - - if (script.attribs.defer !== '' || script.attribs.async === '') { - missing = true - } - } - expect(missing).toBe(false) - }) - - it('should only have one DOCTYPE', async () => { - const html = await renderViaHTTP(appPort, '/') - expect(html).toMatch(/^ { - const browser = await webdriver(appPort, '/') - await browser.eval(`(function() { - window.beforeNav = 1 - window.next.router.push({ - pathname: '/non-existent', - query: { hello: 'world' } - }) - })()`) - - await check( - () => browser.eval('document.documentElement.innerHTML'), - /page could not be found/ - ) - - expect(await browser.eval('window.beforeNav')).toBeFalsy() - expect(await browser.eval('window.location.hash')).toBe('') - expect(await browser.eval('window.location.search')).toBe('?hello=world') - expect(await browser.eval('window.location.pathname')).toBe( - '/non-existent' - ) - }) - } - - it('should remove placeholder for next/image correctly', async () => { - const browser = await webdriver(context.appPort, '/') - - await browser.eval(`(function() { - window.beforeNav = 1 - window.next.router.push('/static-image') - })()`) - await browser.waitForElementByCss('#static-image') - - expect(await browser.eval('window.beforeNav')).toBe(1) - - await check( - () => browser.elementByCss('img').getComputedCss('background-image'), - 'none' - ) - - await browser.eval(`(function() { - window.beforeNav = 1 - window.next.router.push('/') - })()`) - await browser.waitForElementByCss('.index-page') - await waitFor(1000) - - await browser.eval(`(function() { - window.beforeNav = 1 - window.next.router.push('/static-image') - })()`) - await browser.waitForElementByCss('#static-image') - - expect(await browser.eval('window.beforeNav')).toBe(1) - - await check( - () => - browser - .elementByCss('#static-image') - .getComputedCss('background-image'), - 'none' - ) - - for (let i = 0; i < 5; i++) { - expect( - await browser - .elementByCss('#static-image') - .getComputedCss('background-image') - ).toBe('none') - await waitFor(500) - } - }) - - dynamicImportTests(context, (p, q) => renderViaHTTP(context.appPort, p, q)) - - processEnv(context) - if (browserName !== 'safari') security(context) -}) diff --git a/test/integration/production/components/dynamic-css/many-imports/with-css-1.js b/test/production/pages-dir/production/fixture/components/dynamic-css/many-imports/with-css-1.js similarity index 100% rename from test/integration/production/components/dynamic-css/many-imports/with-css-1.js rename to test/production/pages-dir/production/fixture/components/dynamic-css/many-imports/with-css-1.js diff --git a/test/integration/production/components/dynamic-css/many-imports/with-css-1.module.css b/test/production/pages-dir/production/fixture/components/dynamic-css/many-imports/with-css-1.module.css similarity index 100% rename from test/integration/production/components/dynamic-css/many-imports/with-css-1.module.css rename to test/production/pages-dir/production/fixture/components/dynamic-css/many-imports/with-css-1.module.css diff --git a/test/integration/production/components/dynamic-css/many-imports/with-css-2.js b/test/production/pages-dir/production/fixture/components/dynamic-css/many-imports/with-css-2.js similarity index 100% rename from test/integration/production/components/dynamic-css/many-imports/with-css-2.js rename to test/production/pages-dir/production/fixture/components/dynamic-css/many-imports/with-css-2.js diff --git a/test/integration/production/components/dynamic-css/many-imports/with-css-2.module.css b/test/production/pages-dir/production/fixture/components/dynamic-css/many-imports/with-css-2.module.css similarity index 100% rename from test/integration/production/components/dynamic-css/many-imports/with-css-2.module.css rename to test/production/pages-dir/production/fixture/components/dynamic-css/many-imports/with-css-2.module.css diff --git a/test/integration/production/components/dynamic-css/many-imports/with-css-3.js b/test/production/pages-dir/production/fixture/components/dynamic-css/many-imports/with-css-3.js similarity index 100% rename from test/integration/production/components/dynamic-css/many-imports/with-css-3.js rename to test/production/pages-dir/production/fixture/components/dynamic-css/many-imports/with-css-3.js diff --git a/test/integration/production/components/dynamic-css/many-imports/with-css-3.module.css b/test/production/pages-dir/production/fixture/components/dynamic-css/many-imports/with-css-3.module.css similarity index 100% rename from test/integration/production/components/dynamic-css/many-imports/with-css-3.module.css rename to test/production/pages-dir/production/fixture/components/dynamic-css/many-imports/with-css-3.module.css diff --git a/test/integration/production/components/dynamic-css/many-modules/with-css-2.module.css b/test/production/pages-dir/production/fixture/components/dynamic-css/many-modules/with-css-2.module.css similarity index 100% rename from test/integration/production/components/dynamic-css/many-modules/with-css-2.module.css rename to test/production/pages-dir/production/fixture/components/dynamic-css/many-modules/with-css-2.module.css diff --git a/test/integration/production/components/dynamic-css/many-modules/with-css.js b/test/production/pages-dir/production/fixture/components/dynamic-css/many-modules/with-css.js similarity index 100% rename from test/integration/production/components/dynamic-css/many-modules/with-css.js rename to test/production/pages-dir/production/fixture/components/dynamic-css/many-modules/with-css.js diff --git a/test/integration/production/components/dynamic-css/many-modules/with-css.module.css b/test/production/pages-dir/production/fixture/components/dynamic-css/many-modules/with-css.module.css similarity index 100% rename from test/integration/production/components/dynamic-css/many-modules/with-css.module.css rename to test/production/pages-dir/production/fixture/components/dynamic-css/many-modules/with-css.module.css diff --git a/test/integration/production/components/dynamic-css/nested/Nested.jsx b/test/production/pages-dir/production/fixture/components/dynamic-css/nested/Nested.jsx similarity index 100% rename from test/integration/production/components/dynamic-css/nested/Nested.jsx rename to test/production/pages-dir/production/fixture/components/dynamic-css/nested/Nested.jsx diff --git a/test/integration/production/components/dynamic-css/nested/with-css-2.module.css b/test/production/pages-dir/production/fixture/components/dynamic-css/nested/with-css-2.module.css similarity index 100% rename from test/integration/production/components/dynamic-css/nested/with-css-2.module.css rename to test/production/pages-dir/production/fixture/components/dynamic-css/nested/with-css-2.module.css diff --git a/test/integration/production/components/dynamic-css/nested/with-css.js b/test/production/pages-dir/production/fixture/components/dynamic-css/nested/with-css.js similarity index 100% rename from test/integration/production/components/dynamic-css/nested/with-css.js rename to test/production/pages-dir/production/fixture/components/dynamic-css/nested/with-css.js diff --git a/test/integration/production/components/dynamic-css/nested/with-css.module.css b/test/production/pages-dir/production/fixture/components/dynamic-css/nested/with-css.module.css similarity index 100% rename from test/integration/production/components/dynamic-css/nested/with-css.module.css rename to test/production/pages-dir/production/fixture/components/dynamic-css/nested/with-css.module.css diff --git a/test/integration/production/components/dynamic-css/no-css.js b/test/production/pages-dir/production/fixture/components/dynamic-css/no-css.js similarity index 100% rename from test/integration/production/components/dynamic-css/no-css.js rename to test/production/pages-dir/production/fixture/components/dynamic-css/no-css.js diff --git a/test/integration/production/components/dynamic-css/shared-css-module/with-css-2.js b/test/production/pages-dir/production/fixture/components/dynamic-css/shared-css-module/with-css-2.js similarity index 100% rename from test/integration/production/components/dynamic-css/shared-css-module/with-css-2.js rename to test/production/pages-dir/production/fixture/components/dynamic-css/shared-css-module/with-css-2.js diff --git a/test/integration/production/components/dynamic-css/shared-css-module/with-css-2.module.css b/test/production/pages-dir/production/fixture/components/dynamic-css/shared-css-module/with-css-2.module.css similarity index 100% rename from test/integration/production/components/dynamic-css/shared-css-module/with-css-2.module.css rename to test/production/pages-dir/production/fixture/components/dynamic-css/shared-css-module/with-css-2.module.css diff --git a/test/integration/production/components/dynamic-css/shared-css-module/with-css-shared.module.css b/test/production/pages-dir/production/fixture/components/dynamic-css/shared-css-module/with-css-shared.module.css similarity index 100% rename from test/integration/production/components/dynamic-css/shared-css-module/with-css-shared.module.css rename to test/production/pages-dir/production/fixture/components/dynamic-css/shared-css-module/with-css-shared.module.css diff --git a/test/integration/production/components/dynamic-css/shared-css-module/with-css.js b/test/production/pages-dir/production/fixture/components/dynamic-css/shared-css-module/with-css.js similarity index 100% rename from test/integration/production/components/dynamic-css/shared-css-module/with-css.js rename to test/production/pages-dir/production/fixture/components/dynamic-css/shared-css-module/with-css.js diff --git a/test/integration/production/components/dynamic-css/shared-css-module/with-css.module.css b/test/production/pages-dir/production/fixture/components/dynamic-css/shared-css-module/with-css.module.css similarity index 100% rename from test/integration/production/components/dynamic-css/shared-css-module/with-css.module.css rename to test/production/pages-dir/production/fixture/components/dynamic-css/shared-css-module/with-css.module.css diff --git a/test/integration/production/components/dynamic-css/with-css.js b/test/production/pages-dir/production/fixture/components/dynamic-css/with-css.js similarity index 100% rename from test/integration/production/components/dynamic-css/with-css.js rename to test/production/pages-dir/production/fixture/components/dynamic-css/with-css.js diff --git a/test/integration/production/components/dynamic-css/with-css.module.css b/test/production/pages-dir/production/fixture/components/dynamic-css/with-css.module.css similarity index 100% rename from test/integration/production/components/dynamic-css/with-css.module.css rename to test/production/pages-dir/production/fixture/components/dynamic-css/with-css.module.css diff --git a/test/integration/production/components/hello-context.js b/test/production/pages-dir/production/fixture/components/hello-context.js similarity index 100% rename from test/integration/production/components/hello-context.js rename to test/production/pages-dir/production/fixture/components/hello-context.js diff --git a/test/integration/production/components/hello1.js b/test/production/pages-dir/production/fixture/components/hello1.js similarity index 100% rename from test/integration/production/components/hello1.js rename to test/production/pages-dir/production/fixture/components/hello1.js diff --git a/test/integration/production/components/hello2.js b/test/production/pages-dir/production/fixture/components/hello2.js similarity index 100% rename from test/integration/production/components/hello2.js rename to test/production/pages-dir/production/fixture/components/hello2.js diff --git a/test/integration/production/components/logo/dark.svg b/test/production/pages-dir/production/fixture/components/logo/dark.svg similarity index 100% rename from test/integration/production/components/logo/dark.svg rename to test/production/pages-dir/production/fixture/components/logo/dark.svg diff --git a/test/integration/production/components/logo/index.js b/test/production/pages-dir/production/fixture/components/logo/index.js similarity index 100% rename from test/integration/production/components/logo/index.js rename to test/production/pages-dir/production/fixture/components/logo/index.js diff --git a/test/integration/production/components/logo/logo.module.css b/test/production/pages-dir/production/fixture/components/logo/logo.module.css similarity index 100% rename from test/integration/production/components/logo/logo.module.css rename to test/production/pages-dir/production/fixture/components/logo/logo.module.css diff --git a/test/integration/production/components/welcome.js b/test/production/pages-dir/production/fixture/components/welcome.js similarity index 100% rename from test/integration/production/components/welcome.js rename to test/production/pages-dir/production/fixture/components/welcome.js diff --git a/test/integration/production/info.json b/test/production/pages-dir/production/fixture/info.json similarity index 100% rename from test/integration/production/info.json rename to test/production/pages-dir/production/fixture/info.json diff --git a/test/integration/production/next.config.js b/test/production/pages-dir/production/fixture/next.config.js similarity index 100% rename from test/integration/production/next.config.js rename to test/production/pages-dir/production/fixture/next.config.js diff --git a/test/integration/production/pages/about.js b/test/production/pages-dir/production/fixture/pages/about.js similarity index 100% rename from test/integration/production/pages/about.js rename to test/production/pages-dir/production/fixture/pages/about.js diff --git a/test/integration/production/pages/amp-hybrid.js b/test/production/pages-dir/production/fixture/pages/amp-hybrid.js similarity index 100% rename from test/integration/production/pages/amp-hybrid.js rename to test/production/pages-dir/production/fixture/pages/amp-hybrid.js diff --git a/test/integration/production/pages/amp.js b/test/production/pages-dir/production/fixture/pages/amp.js similarity index 100% rename from test/integration/production/pages/amp.js rename to test/production/pages-dir/production/fixture/pages/amp.js diff --git a/test/integration/production/pages/another.js b/test/production/pages-dir/production/fixture/pages/another.js similarity index 100% rename from test/integration/production/pages/another.js rename to test/production/pages-dir/production/fixture/pages/another.js diff --git a/test/integration/production/pages/api/[post]/index.js b/test/production/pages-dir/production/fixture/pages/api/[post]/index.js similarity index 100% rename from test/integration/production/pages/api/[post]/index.js rename to test/production/pages-dir/production/fixture/pages/api/[post]/index.js diff --git a/test/integration/production/pages/api/hello.js b/test/production/pages-dir/production/fixture/pages/api/hello.js similarity index 100% rename from test/integration/production/pages/api/hello.js rename to test/production/pages-dir/production/fixture/pages/api/hello.js diff --git a/test/integration/production/pages/api/index.js b/test/production/pages-dir/production/fixture/pages/api/index.js similarity index 100% rename from test/integration/production/pages/api/index.js rename to test/production/pages-dir/production/fixture/pages/api/index.js diff --git a/test/integration/production/pages/api/readfile-dirname.js b/test/production/pages-dir/production/fixture/pages/api/readfile-dirname.js similarity index 100% rename from test/integration/production/pages/api/readfile-dirname.js rename to test/production/pages-dir/production/fixture/pages/api/readfile-dirname.js diff --git a/test/integration/production/pages/api/readfile-processcwd.js b/test/production/pages-dir/production/fixture/pages/api/readfile-processcwd.js similarity index 100% rename from test/integration/production/pages/api/readfile-processcwd.js rename to test/production/pages-dir/production/fixture/pages/api/readfile-processcwd.js diff --git a/test/integration/production/pages/bad-promise.js b/test/production/pages-dir/production/fixture/pages/bad-promise.js similarity index 100% rename from test/integration/production/pages/bad-promise.js rename to test/production/pages-dir/production/fixture/pages/bad-promise.js diff --git a/test/integration/production/pages/client-error.js b/test/production/pages-dir/production/fixture/pages/client-error.js similarity index 100% rename from test/integration/production/pages/client-error.js rename to test/production/pages-dir/production/fixture/pages/client-error.js diff --git a/test/integration/production/pages/counter.js b/test/production/pages-dir/production/fixture/pages/counter.js similarity index 100% rename from test/integration/production/pages/counter.js rename to test/production/pages-dir/production/fixture/pages/counter.js diff --git a/test/integration/production/pages/css-and-back.js b/test/production/pages-dir/production/fixture/pages/css-and-back.js similarity index 100% rename from test/integration/production/pages/css-and-back.js rename to test/production/pages-dir/production/fixture/pages/css-and-back.js diff --git a/test/integration/production/pages/css-modules.js b/test/production/pages-dir/production/fixture/pages/css-modules.js similarity index 100% rename from test/integration/production/pages/css-modules.js rename to test/production/pages-dir/production/fixture/pages/css-modules.js diff --git a/test/integration/production/pages/development-logs/index.js b/test/production/pages-dir/production/fixture/pages/development-logs/index.js similarity index 100% rename from test/integration/production/pages/development-logs/index.js rename to test/production/pages-dir/production/fixture/pages/development-logs/index.js diff --git a/test/integration/production/pages/dynamic/css.js b/test/production/pages-dir/production/fixture/pages/dynamic/css.js similarity index 100% rename from test/integration/production/pages/dynamic/css.js rename to test/production/pages-dir/production/fixture/pages/dynamic/css.js diff --git a/test/integration/production/pages/dynamic/index.js b/test/production/pages-dir/production/fixture/pages/dynamic/index.js similarity index 100% rename from test/integration/production/pages/dynamic/index.js rename to test/production/pages-dir/production/fixture/pages/dynamic/index.js diff --git a/test/integration/production/pages/dynamic/many-css-modules.js b/test/production/pages-dir/production/fixture/pages/dynamic/many-css-modules.js similarity index 100% rename from test/integration/production/pages/dynamic/many-css-modules.js rename to test/production/pages-dir/production/fixture/pages/dynamic/many-css-modules.js diff --git a/test/integration/production/pages/dynamic/many-dynamic-css.js b/test/production/pages-dir/production/fixture/pages/dynamic/many-dynamic-css.js similarity index 100% rename from test/integration/production/pages/dynamic/many-dynamic-css.js rename to test/production/pages-dir/production/fixture/pages/dynamic/many-dynamic-css.js diff --git a/test/integration/production/pages/dynamic/nested-css.js b/test/production/pages-dir/production/fixture/pages/dynamic/nested-css.js similarity index 100% rename from test/integration/production/pages/dynamic/nested-css.js rename to test/production/pages-dir/production/fixture/pages/dynamic/nested-css.js diff --git a/test/integration/production/pages/dynamic/no-chunk.js b/test/production/pages-dir/production/fixture/pages/dynamic/no-chunk.js similarity index 100% rename from test/integration/production/pages/dynamic/no-chunk.js rename to test/production/pages-dir/production/fixture/pages/dynamic/no-chunk.js diff --git a/test/integration/production/pages/dynamic/no-css.js b/test/production/pages-dir/production/fixture/pages/dynamic/no-css.js similarity index 100% rename from test/integration/production/pages/dynamic/no-css.js rename to test/production/pages-dir/production/fixture/pages/dynamic/no-css.js diff --git a/test/integration/production/pages/dynamic/no-ssr-custom-loading.js b/test/production/pages-dir/production/fixture/pages/dynamic/no-ssr-custom-loading.js similarity index 100% rename from test/integration/production/pages/dynamic/no-ssr-custom-loading.js rename to test/production/pages-dir/production/fixture/pages/dynamic/no-ssr-custom-loading.js diff --git a/test/integration/production/pages/dynamic/no-ssr.js b/test/production/pages-dir/production/fixture/pages/dynamic/no-ssr.js similarity index 100% rename from test/integration/production/pages/dynamic/no-ssr.js rename to test/production/pages-dir/production/fixture/pages/dynamic/no-ssr.js diff --git a/test/integration/production/pages/dynamic/pagechange1.js b/test/production/pages-dir/production/fixture/pages/dynamic/pagechange1.js similarity index 100% rename from test/integration/production/pages/dynamic/pagechange1.js rename to test/production/pages-dir/production/fixture/pages/dynamic/pagechange1.js diff --git a/test/integration/production/pages/dynamic/pagechange2.js b/test/production/pages-dir/production/fixture/pages/dynamic/pagechange2.js similarity index 100% rename from test/integration/production/pages/dynamic/pagechange2.js rename to test/production/pages-dir/production/fixture/pages/dynamic/pagechange2.js diff --git a/test/integration/production/pages/dynamic/shared-css-module.js b/test/production/pages-dir/production/fixture/pages/dynamic/shared-css-module.js similarity index 100% rename from test/integration/production/pages/dynamic/shared-css-module.js rename to test/production/pages-dir/production/fixture/pages/dynamic/shared-css-module.js diff --git a/test/integration/production/pages/dynamic/ssr-true.js b/test/production/pages-dir/production/fixture/pages/dynamic/ssr-true.js similarity index 100% rename from test/integration/production/pages/dynamic/ssr-true.js rename to test/production/pages-dir/production/fixture/pages/dynamic/ssr-true.js diff --git a/test/integration/production/pages/dynamic/ssr.js b/test/production/pages-dir/production/fixture/pages/dynamic/ssr.js similarity index 100% rename from test/integration/production/pages/dynamic/ssr.js rename to test/production/pages-dir/production/fixture/pages/dynamic/ssr.js diff --git a/test/integration/production/pages/error-in-browser-render-status-code.js b/test/production/pages-dir/production/fixture/pages/error-in-browser-render-status-code.js similarity index 100% rename from test/integration/production/pages/error-in-browser-render-status-code.js rename to test/production/pages-dir/production/fixture/pages/error-in-browser-render-status-code.js diff --git a/test/integration/production/pages/error-in-browser-render.js b/test/production/pages-dir/production/fixture/pages/error-in-browser-render.js similarity index 100% rename from test/integration/production/pages/error-in-browser-render.js rename to test/production/pages-dir/production/fixture/pages/error-in-browser-render.js diff --git a/test/integration/production/pages/error-in-ssr-render.js b/test/production/pages-dir/production/fixture/pages/error-in-ssr-render.js similarity index 100% rename from test/integration/production/pages/error-in-ssr-render.js rename to test/production/pages-dir/production/fixture/pages/error-in-ssr-render.js diff --git a/test/integration/production/pages/external-and-back.js b/test/production/pages-dir/production/fixture/pages/external-and-back.js similarity index 100% rename from test/integration/production/pages/external-and-back.js rename to test/production/pages-dir/production/fixture/pages/external-and-back.js diff --git a/test/integration/production/pages/finish-response.js b/test/production/pages-dir/production/fixture/pages/finish-response.js similarity index 100% rename from test/integration/production/pages/finish-response.js rename to test/production/pages-dir/production/fixture/pages/finish-response.js diff --git a/test/integration/production/pages/fully-dynamic.js b/test/production/pages-dir/production/fixture/pages/fully-dynamic.js similarity index 100% rename from test/integration/production/pages/fully-dynamic.js rename to test/production/pages-dir/production/fixture/pages/fully-dynamic.js diff --git a/test/integration/production/pages/fully-static.js b/test/production/pages-dir/production/fixture/pages/fully-static.js similarity index 100% rename from test/integration/production/pages/fully-static.js rename to test/production/pages-dir/production/fixture/pages/fully-static.js diff --git a/test/integration/production/pages/index.js b/test/production/pages-dir/production/fixture/pages/index.js similarity index 100% rename from test/integration/production/pages/index.js rename to test/production/pages-dir/production/fixture/pages/index.js diff --git a/test/integration/production/pages/invalid-param/[slug].js b/test/production/pages-dir/production/fixture/pages/invalid-param/[slug].js similarity index 100% rename from test/integration/production/pages/invalid-param/[slug].js rename to test/production/pages-dir/production/fixture/pages/invalid-param/[slug].js diff --git a/test/integration/production/pages/mark-in-head.js b/test/production/pages-dir/production/fixture/pages/mark-in-head.js similarity index 100% rename from test/integration/production/pages/mark-in-head.js rename to test/production/pages-dir/production/fixture/pages/mark-in-head.js diff --git a/test/integration/production/pages/next-import.js b/test/production/pages-dir/production/fixture/pages/next-import.js similarity index 100% rename from test/integration/production/pages/next-import.js rename to test/production/pages-dir/production/fixture/pages/next-import.js diff --git a/test/integration/production/pages/node-browser-polyfills.js b/test/production/pages-dir/production/fixture/pages/node-browser-polyfills.js similarity index 100% rename from test/integration/production/pages/node-browser-polyfills.js rename to test/production/pages-dir/production/fixture/pages/node-browser-polyfills.js diff --git a/test/integration/production/pages/prefetch.js b/test/production/pages-dir/production/fixture/pages/prefetch.js similarity index 100% rename from test/integration/production/pages/prefetch.js rename to test/production/pages-dir/production/fixture/pages/prefetch.js diff --git a/test/integration/production/pages/process-env.js b/test/production/pages-dir/production/fixture/pages/process-env.js similarity index 100% rename from test/integration/production/pages/process-env.js rename to test/production/pages-dir/production/fixture/pages/process-env.js diff --git a/test/integration/production/pages/query.js b/test/production/pages-dir/production/fixture/pages/query.js similarity index 100% rename from test/integration/production/pages/query.js rename to test/production/pages-dir/production/fixture/pages/query.js diff --git a/test/integration/production/pages/regexp-polyfill.js b/test/production/pages-dir/production/fixture/pages/regexp-polyfill.js similarity index 100% rename from test/integration/production/pages/regexp-polyfill.js rename to test/production/pages-dir/production/fixture/pages/regexp-polyfill.js diff --git a/test/integration/production/pages/runtime-config.js b/test/production/pages-dir/production/fixture/pages/runtime-config.js similarity index 100% rename from test/integration/production/pages/runtime-config.js rename to test/production/pages-dir/production/fixture/pages/runtime-config.js diff --git a/test/integration/production/pages/shadowed-page.js b/test/production/pages-dir/production/fixture/pages/shadowed-page.js similarity index 100% rename from test/integration/production/pages/shadowed-page.js rename to test/production/pages-dir/production/fixture/pages/shadowed-page.js diff --git a/test/integration/production/pages/some-amp.js b/test/production/pages-dir/production/fixture/pages/some-amp.js similarity index 100% rename from test/integration/production/pages/some-amp.js rename to test/production/pages-dir/production/fixture/pages/some-amp.js diff --git a/test/integration/production/pages/static-image.js b/test/production/pages-dir/production/fixture/pages/static-image.js similarity index 100% rename from test/integration/production/pages/static-image.js rename to test/production/pages-dir/production/fixture/pages/static-image.js diff --git a/test/integration/production/pages/svg-image.js b/test/production/pages-dir/production/fixture/pages/svg-image.js similarity index 100% rename from test/integration/production/pages/svg-image.js rename to test/production/pages-dir/production/fixture/pages/svg-image.js diff --git a/test/integration/production/pages/to-nonexistent.js b/test/production/pages-dir/production/fixture/pages/to-nonexistent.js similarity index 100% rename from test/integration/production/pages/to-nonexistent.js rename to test/production/pages-dir/production/fixture/pages/to-nonexistent.js diff --git a/test/integration/production/pages/to-shadowed-page.js b/test/production/pages-dir/production/fixture/pages/to-shadowed-page.js similarity index 100% rename from test/integration/production/pages/to-shadowed-page.js rename to test/production/pages-dir/production/fixture/pages/to-shadowed-page.js diff --git a/test/integration/production/pages/with-title.js b/test/production/pages-dir/production/fixture/pages/with-title.js similarity index 100% rename from test/integration/production/pages/with-title.js rename to test/production/pages-dir/production/fixture/pages/with-title.js diff --git a/test/integration/production/public/data/data.txt b/test/production/pages-dir/production/fixture/public/data/data.txt similarity index 100% rename from test/integration/production/public/data/data.txt rename to test/production/pages-dir/production/fixture/public/data/data.txt diff --git a/test/integration/production/public/file b/test/production/pages-dir/production/fixture/public/file similarity index 100% rename from test/integration/production/public/file rename to test/production/pages-dir/production/fixture/public/file diff --git a/test/integration/production/public/regexp-test.js b/test/production/pages-dir/production/fixture/public/regexp-test.js similarity index 100% rename from test/integration/production/public/regexp-test.js rename to test/production/pages-dir/production/fixture/public/regexp-test.js diff --git a/test/integration/production/public/static/legacy.txt b/test/production/pages-dir/production/fixture/public/static/legacy.txt similarity index 100% rename from test/integration/production/public/static/legacy.txt rename to test/production/pages-dir/production/fixture/public/static/legacy.txt diff --git a/test/integration/production/public/vercel.png b/test/production/pages-dir/production/fixture/public/vercel.png similarity index 100% rename from test/integration/production/public/vercel.png rename to test/production/pages-dir/production/fixture/public/vercel.png diff --git a/test/integration/production/public/xss.svg b/test/production/pages-dir/production/fixture/public/xss.svg similarity index 100% rename from test/integration/production/public/xss.svg rename to test/production/pages-dir/production/fixture/public/xss.svg diff --git a/test/integration/production/static/data/item.txt b/test/production/pages-dir/production/fixture/static/data/item.txt similarity index 100% rename from test/integration/production/static/data/item.txt rename to test/production/pages-dir/production/fixture/static/data/item.txt diff --git a/test/integration/production/static/hello.json b/test/production/pages-dir/production/fixture/static/hello.json similarity index 100% rename from test/integration/production/static/hello.json rename to test/production/pages-dir/production/fixture/static/hello.json diff --git a/test/integration/production/test/dynamic.js b/test/production/pages-dir/production/test/dynamic.ts similarity index 91% rename from test/integration/production/test/dynamic.js rename to test/production/pages-dir/production/test/dynamic.ts index d22f147e51..36a45e257a 100644 --- a/test/integration/production/test/dynamic.js +++ b/test/production/pages-dir/production/test/dynamic.ts @@ -2,10 +2,11 @@ import webdriver from 'next-webdriver' import cheerio from 'cheerio' import { check } from 'next-test-utils' +import { NextInstance } from 'e2e-utils' // These tests are similar to ../../basic/test/dynamic.js -export default (context, render) => { - async function get$(path, query) { +export default (next: NextInstance, render) => { + async function get$(path: string, query?: any) { const html = await render(path, query) return cheerio.load(html) } @@ -43,12 +44,13 @@ export default (context, render) => { it('should not remove css styles for same css file between page transitions', async () => { let browser try { - browser = await webdriver(context.appPort, '/dynamic/pagechange1') + browser = await webdriver(next.appPort, '/dynamic/pagechange1') await check(() => browser.elementByCss('body').text(), /PageChange1/) const firstElement = await browser.elementById('with-css') const css1 = await firstElement.getComputedCss('display') expect(css1).toBe('flex') await browser.eval(function () { + // @ts-expect-error window.next exists window.next.router.push('/dynamic/pagechange2') }) await check(() => browser.elementByCss('body').text(), /PageChange2/) @@ -78,7 +80,7 @@ export default (context, render) => { it('should render even there are no physical chunk exists', async () => { let browser try { - browser = await webdriver(context.appPort, '/dynamic/no-chunk') + browser = await webdriver(next.appPort, '/dynamic/no-chunk') await check( () => browser.elementByCss('body').text(), /Welcome, normal/ @@ -103,7 +105,7 @@ export default (context, render) => { it('should render the component on client side', async () => { let browser try { - browser = await webdriver(context.appPort, '/dynamic/no-ssr') + browser = await webdriver(next.appPort, '/dynamic/no-ssr') await check( () => browser.elementByCss('body').text(), /Hello World 1/ @@ -125,7 +127,7 @@ export default (context, render) => { it('should render the component on client side', async () => { let browser try { - browser = await webdriver(context.appPort, '/dynamic/ssr-true') + browser = await webdriver(next.appPort, '/dynamic/ssr-true') await check( () => browser.elementByCss('body').text(), /Hello World 1/ @@ -148,7 +150,7 @@ export default (context, render) => { let browser try { browser = await webdriver( - context.appPort, + next.appPort, '/dynamic/no-ssr-custom-loading' ) await check( diff --git a/test/production/pages-dir/production/test/index.test.ts b/test/production/pages-dir/production/test/index.test.ts new file mode 100644 index 0000000000..1303b19950 --- /dev/null +++ b/test/production/pages-dir/production/test/index.test.ts @@ -0,0 +1,1312 @@ +/* eslint-env jest */ +import cheerio from 'cheerio' +import fs, { existsSync } from 'fs-extra' +import globOriginal from 'glob' +import { + renderViaHTTP, + waitFor, + getPageFileFromPagesManifest, + check, + fetchViaHTTP, +} from 'next-test-utils' +import webdriver from 'next-webdriver' +import { + BUILD_MANIFEST, + PAGES_MANIFEST, + REACT_LOADABLE_MANIFEST, +} from 'next/constants' +import { recursiveReadDir } from 'next/dist/lib/recursive-readdir' +import path, { join, sep } from 'path' +import dynamicImportTests from './dynamic' +import processEnv from './process-env' +import security from './security' +import { promisify } from 'util' +import { createNextDescribe } from 'e2e-utils' + +const glob = promisify(globOriginal) + +if (process.env.TEST_WASM) { + jest.setTimeout(240 * 1000) +} + +createNextDescribe( + 'Production Usage', + { + files: path.join(__dirname, '../fixture'), + dependencies: { + nanoid: '3.1.30', + 'es5-ext': '0.10.53', + }, + }, + ({ next }) => { + it('should navigate through history after query update', async () => { + const browser = await webdriver(next.appPort, '/') + await browser.eval('window.next.router.push("/about?a=b")') + await browser.waitForElementByCss('.about-page') + await browser.waitForCondition(`!!window.next.router.isReady`) + + await browser.refresh() + await browser.waitForCondition(`!!window.next.router.isReady`) + await browser.back() + await browser.waitForElementByCss('.index-page') + await browser.forward() + await browser.waitForElementByCss('.about-page') + await browser.back() + await browser.waitForElementByCss('.index-page') + await browser.refresh() + await browser.waitForCondition(`!!window.next.router.isReady`) + await browser.forward() + await browser.waitForElementByCss('.about-page') + }) + + if (process.env.BROWSER_NAME !== 'safari') { + it.each([ + { hash: '#hello?' }, + { hash: '#?' }, + { hash: '##' }, + { hash: '##?' }, + { hash: '##hello?' }, + { hash: '##hello' }, + { hash: '#hello?world' }, + { search: '?hello=world', hash: '#a', query: { hello: 'world' } }, + { search: '?hello', hash: '#a', query: { hello: '' } }, + { search: '?hello=', hash: '#a', query: { hello: '' } }, + ])( + 'should handle query/hash correctly during query updating $hash $search', + async ({ hash, search, query }) => { + const browser = await webdriver( + next.appPort, + `/${search || ''}${hash || ''}` + ) + + await check( + () => + browser.eval( + 'window.next.router.isReady ? "ready" : "not ready"' + ), + 'ready' + ) + expect(await browser.eval('window.location.pathname')).toBe('/') + expect(await browser.eval('window.location.hash')).toBe(hash || '') + expect(await browser.eval('window.location.search')).toBe( + search || '' + ) + expect(await browser.eval('next.router.pathname')).toBe('/') + expect( + JSON.parse(await browser.eval('JSON.stringify(next.router.query)')) + ).toEqual(query || {}) + } + ) + } + + it('should not show target deprecation warning', () => { + expect(next.cliOutput).not.toContain( + 'The `target` config is deprecated and will be removed in a future version' + ) + }) + + it('should respond with 405 for POST to static page', async () => { + const res = await fetchViaHTTP(next.appPort, '/', undefined, { + method: 'POST', + }) + expect(res.status).toBe(405) + expect(await res.text()).toContain('Method Not Allowed') + }) + + it('should contain generated page count in output', async () => { + const pageCount = 40 + expect(next.cliOutput).toContain( + `Generating static pages (0/${pageCount})` + ) + expect(next.cliOutput).toContain( + `Generating static pages (${pageCount}/${pageCount})` + ) + // we should only have 4 segments and the initial message logged out + expect(next.cliOutput.match(/Generating static pages/g).length).toBe(5) + }) + + it('should output traces', async () => { + const serverTrace = await next.readJSON('.next/next-server.js.nft.json') + + expect(serverTrace.version).toBe(1) + expect( + serverTrace.files.some((file) => + file.includes('next/dist/server/send-payload/index.js') + ) + ).toBe(true) + expect( + serverTrace.files.some((file) => + file.includes('next/dist/server/lib/route-resolver.js') + ) + ).toBe(false) + const repoRoot = join(next.testDir, '../../../../') + expect( + serverTrace.files.some((file) => { + const fullPath = join(next.testDir, '.next', file) + if (!fullPath.startsWith(repoRoot)) { + console.error(`Found next-server trace file outside repo root`, { + repoRoot, + fullPath, + file, + }) + return true + } + return false + }) + ).toBe(false) + expect( + serverTrace.files.some((file) => + file.includes('next/dist/shared/lib/page-path/normalize-page-path.js') + ) + ).toBe(true) + expect( + serverTrace.files.some((file) => + file.includes('next/dist/server/render.js') + ) + ).toBe(true) + expect( + serverTrace.files.some((file) => + file.includes('next/dist/server/load-components.js') + ) + ).toBe(true) + + if (process.platform !== 'win32') { + expect( + serverTrace.files.some((file) => + file.includes('next/dist/compiled/webpack/bundle5.js') + ) + ).toBe(false) + expect( + serverTrace.files.some((file) => file.includes('node_modules/sharp')) + ).toBe(false) + } + + const checks = [ + { + page: '/_app', + tests: [ + /webpack-runtime\.js/, + /node_modules\/react\/index\.js/, + /node_modules\/react\/package\.json/, + /node_modules\/react\/cjs\/react\.production\.min\.js/, + ], + notTests: [/\0/, /\?/, /!/], + }, + { + page: '/client-error', + tests: [ + /webpack-runtime\.js/, + /chunks\/.*?\.js/, + /node_modules\/react\/index\.js/, + /node_modules\/react\/package\.json/, + /node_modules\/react\/cjs\/react\.production\.min\.js/, + /node_modules\/next/, + ], + notTests: [/\0/, /\?/, /!/], + }, + { + page: '/dynamic', + tests: [ + /webpack-runtime\.js/, + /chunks\/.*?\.js/, + /node_modules\/react\/index\.js/, + /node_modules\/react\/package\.json/, + /node_modules\/react\/cjs\/react\.production\.min\.js/, + /node_modules\/next/, + ], + notTests: [/\0/, /\?/, /!/], + }, + { + page: '/index', + tests: [ + /webpack-runtime\.js/, + /chunks\/.*?\.js/, + /node_modules\/react\/index\.js/, + /node_modules\/react\/package\.json/, + /node_modules\/react\/cjs\/react\.production\.min\.js/, + /node_modules\/next/, + /node_modules\/nanoid\/index\.js/, + /node_modules\/nanoid\/url-alphabet\/index\.js/, + /node_modules\/es5-ext\/array\/#\/clear\.js/, + ], + notTests: [/next\/dist\/pages\/_error\.js/, /\0/, /\?/, /!/], + }, + { + page: '/counter', + tests: [ + /webpack-runtime\.js/, + /chunks\/.*?\.js/, + /node_modules\/react\/index\.js/, + /node_modules\/react\/package\.json/, + /node_modules\/react\/cjs\/react\.production\.min\.js/, + /node_modules\/react\/cjs\/react\.development\.js/, + /node_modules\/next/, + ], + notTests: [/\0/, /\?/, /!/], + }, + { + page: '/next-import', + tests: [ + /webpack-runtime\.js/, + /chunks\/.*?\.js/, + /node_modules\/react\/index\.js/, + /node_modules\/react\/package\.json/, + /node_modules\/react\/cjs\/react\.production\.min\.js/, + /node_modules\/next/, + ], + notTests: [ + /next\/dist\/server\/next\.js/, + /next\/dist\/bin/, + /\0/, + /\?/, + /!/, + ], + }, + { + page: '/api', + tests: [/webpack-runtime\.js/, /\/logo\.module\.css/], + notTests: [ + /next\/dist\/server\/next\.js/, + /next\/dist\/bin/, + /\0/, + /\?/, + /!/, + ], + }, + { + page: '/api/readfile-dirname', + tests: [/webpack-api-runtime\.js/, /static\/data\/item\.txt/], + notTests: [ + /next\/dist\/server\/next\.js/, + /next\/dist\/bin/, + /\0/, + /\?/, + /!/, + ], + }, + { + page: '/api/readfile-processcwd', + tests: [/webpack-api-runtime\.js/, /static\/data\/item\.txt/], + notTests: [ + /next\/dist\/server\/next\.js/, + /next\/dist\/bin/, + /\0/, + /\?/, + /!/, + ], + }, + ] + + for (const check of checks) { + const { version, files } = await next.readJSON( + join('.next/server/pages/', check.page + '.js.nft.json') + ) + expect(version).toBe(1) + expect([...new Set(files)].length).toBe(files.length) + + expect( + check.tests.every((item) => { + if (files.some((file) => item.test(file))) { + return true + } + console.error( + `Failed to find ${item} for page ${check.page} in`, + files + ) + return false + }) + ).toBe(true) + + if (sep === '/') { + expect( + check.notTests.some((item) => { + if (files.some((file) => item.test(file))) { + console.error(`Found unexpected ${item} in`, files) + return true + } + return false + }) + ).toBe(false) + } + } + }) + + it('should not contain currentScript usage for publicPath', async () => { + const globResult = await glob('webpack-*.js', { + cwd: join(next.testDir, '.next/static/chunks'), + }) + + if (!globResult || globResult.length !== 1) { + throw new Error('could not find webpack-hash.js chunk') + } + + const content = await next.readFile( + join('.next/static/chunks', globResult[0]) + ) + + expect(content).not.toContain('.currentScript') + }) + + it('should not contain amp, rsc APIs in main chunk', async () => { + const globResult = await glob('main-*.js', { + cwd: join(next.testDir, '.next/static/chunks'), + }) + + if (!globResult || globResult.length !== 1) { + throw new Error('could not find main js chunk') + } + + const content = await fs.readFile( + join(next.testDir, '.next/static/chunks', globResult[0]), + 'utf8' + ) + + expect(content).not.toContain('useAmp') + expect(content).not.toContain('useRefreshRoot') + }) + + describe('With basic usage', () => { + it('should render the page', async () => { + const html = await renderViaHTTP(next.appPort, '/') + expect(html).toMatch(/Hello World/) + }) + + if (global.browserName === 'internet explorer') { + it('should handle bad Promise polyfill', async () => { + const browser = await webdriver(next.appPort, '/bad-promise') + expect(await browser.eval('window.didRender')).toBe(true) + }) + + it('should polyfill RegExp successfully', async () => { + const browser = await webdriver(next.appPort, '/regexp-polyfill') + expect(await browser.eval('window.didRender')).toBe(true) + // wait a second for the script to be loaded + await waitFor(1000) + + expect(await browser.eval('window.isSticky')).toBe(true) + expect(await browser.eval('window.isMatch1')).toBe(true) + expect(await browser.eval('window.isMatch2')).toBe(false) + }) + } + + it('should polyfill Node.js modules', async () => { + const browser = await webdriver(next.appPort, '/node-browser-polyfills') + await browser.waitForCondition('window.didRender') + + const data = await browser + .waitForElementByCss('#node-browser-polyfills') + .text() + const parsedData = JSON.parse(data) + + expect(parsedData.vm).toBe(105) + expect(parsedData.hash).toBe( + 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' + ) + expect(parsedData.path).toBe('/hello/world/test.txt') + expect(parsedData.buffer).toBe('hello world') + expect(parsedData.stream).toBe(true) + }) + + it('should allow etag header support', async () => { + const url = `http://localhost:${next.appPort}` + const etag = (await fetchViaHTTP(url, '/')).headers.get('ETag') + + const headers = { 'If-None-Match': etag } + const res2 = await fetchViaHTTP(url, '/', undefined, { headers }) + expect(res2.status).toBe(304) + }) + + it('should allow etag header support with getStaticProps', async () => { + const url = `http://localhost:${next.appPort}` + const etag = (await fetchViaHTTP(url, '/fully-static')).headers.get( + 'ETag' + ) + + const headers = { 'If-None-Match': etag } + const res2 = await fetchViaHTTP(url, '/fully-static', undefined, { + headers, + }) + expect(res2.status).toBe(304) + }) + + // TODO: should we generate weak etags for streaming getServerSideProps? + // this is currently not expected to work with react-18 + it.skip('should allow etag header support with getServerSideProps', async () => { + const url = `http://localhost:${next.appPort}` + const etag = (await fetchViaHTTP(url, '/fully-dynamic')).headers.get( + 'ETag' + ) + + const headers = { 'If-None-Match': etag } + const res2 = await fetchViaHTTP(url, '/fully-dynamic', undefined, { + headers, + }) + expect(res2.status).toBe(304) + }) + + it('should have X-Powered-By header support', async () => { + const url = `http://localhost:${next.appPort}` + const header = (await fetchViaHTTP(url, '/')).headers.get( + 'X-Powered-By' + ) + + expect(header).toBe('Next.js') + }) + + it('should render 404 for routes that do not exist', async () => { + const url = `http://localhost:${next.appPort}` + const res = await fetchViaHTTP(url, '/abcdefghijklmno') + const text = await res.text() + const $html = cheerio.load(text) + expect($html('html').text()).toMatch(/404/) + expect(text).toMatch(/"statusCode":404/) + expect(res.status).toBe(404) + }) + + it('should render 404 for /_next/static route', async () => { + const html = await renderViaHTTP(next.appPort, '/_next/static') + expect(html).toMatch(/This page could not be found/) + }) + + it('should render 200 for POST on page', async () => { + const res = await fetchViaHTTP( + `http://localhost:${next.appPort}`, + '/fully-dynamic', + undefined, + { + method: 'POST', + } + ) + expect(res.status).toBe(200) + }) + + it('should render 404 for POST on missing page', async () => { + const res = await fetchViaHTTP( + `http://localhost:${next.appPort}`, + '/fake-page', + undefined, + { + method: 'POST', + } + ) + expect(res.status).toBe(404) + }) + + it('should render 404 for _next routes that do not exist', async () => { + const url = `http://localhost:${next.appPort}` + const res = await fetchViaHTTP(url, '/_next/abcdef') + expect(res.status).toBe(404) + }) + + it('should render 404 even if the HTTP method is not GET or HEAD', async () => { + const url = `http://localhost:${next.appPort}` + const methods = ['POST', 'PUT', 'DELETE'] + for (const method of methods) { + const res = await fetchViaHTTP(url, '/_next/abcdef', undefined, { + method, + }) + expect(res.status).toBe(404) + } + }) + + it('should render 404 for dotfiles in /static', async () => { + const url = `http://localhost:${next.appPort}` + const res = await fetchViaHTTP(url, '/static/.env') + expect(res.status).toBe(404) + }) + + it('should return 405 method on static then GET and HEAD', async () => { + const res = await fetchViaHTTP( + `http://localhost:${next.appPort}`, + '/static/data/item.txt', + undefined, + { + method: 'POST', + } + ) + expect(res.headers.get('allow').includes('GET')).toBe(true) + expect(res.status).toBe(405) + }) + + it('should return 412 on static file when If-Unmodified-Since is provided and file is modified', async () => { + const buildManifest = await next.readJSON('.next/build-manifest.json') + + const files = buildManifest.pages['/'] + + for (const file of files) { + const res = await fetchViaHTTP( + `http://localhost:${next.appPort}`, + `/_next/${file}`, + undefined, + { + method: 'GET', + headers: { + 'if-unmodified-since': 'Fri, 12 Jul 2019 20:00:13 GMT', + }, + } + ) + expect(res.status).toBe(412) + } + }) + + it('should return 200 on static file if If-Unmodified-Since is invalid date', async () => { + const buildManifest = await next.readJSON('.next/build-manifest.json') + + const files = buildManifest.pages['/'] + + for (const file of files) { + const res = await fetchViaHTTP( + `http://localhost:${next.appPort}`, + `/_next/${file}`, + undefined, + { + method: 'GET', + headers: { 'if-unmodified-since': 'nextjs' }, + } + ) + expect(res.status).toBe(200) + } + }) + + it('should set Content-Length header', async () => { + const url = `http://localhost:${next.appPort}` + const res = await fetchViaHTTP(url, '/') + expect(res.headers.get('Content-Length')).toBeDefined() + }) + + it('should set Cache-Control header', async () => { + const buildManifest = await next.readJSON(`.next/${BUILD_MANIFEST}`) + const reactLoadableManifest = await next.readJSON( + join('./.next', REACT_LOADABLE_MANIFEST) + ) + const url = `http://localhost:${next.appPort}` + + const resources: Set = new Set() + + const manifestKey = Object.keys(reactLoadableManifest).find((item) => { + return item + .replace(/\\/g, '/') + .endsWith('dynamic/css.js -> ../../components/dynamic-css/with-css') + }) + + // test dynamic chunk + reactLoadableManifest[manifestKey].files.forEach((f) => { + resources.add('/' + f) + }) + + // test main.js runtime etc + for (const item of buildManifest.pages['/']) { + resources.add('/' + item) + } + + const cssStaticAssets = await recursiveReadDir( + join(next.testDir, '.next', 'static'), + { pathnameFilter: (f) => /\.css$/.test(f) } + ) + expect(cssStaticAssets.length).toBeGreaterThanOrEqual(1) + expect(cssStaticAssets[0]).toMatch(/[\\/]css[\\/]/) + const mediaStaticAssets = await recursiveReadDir( + join(next.testDir, '.next', 'static'), + { pathnameFilter: (f) => /\.svg$/.test(f) } + ) + expect(mediaStaticAssets.length).toBeGreaterThanOrEqual(1) + expect(mediaStaticAssets[0]).toMatch(/[\\/]media[\\/]/) + ;[...cssStaticAssets, ...mediaStaticAssets].forEach((asset) => { + resources.add(`/static${asset.replace(/\\+/g, '/')}`) + }) + + const responses = await Promise.all( + [...resources].map((resource) => + fetchViaHTTP(url, join('/_next', resource)) + ) + ) + + responses.forEach((res) => { + try { + expect(res.headers.get('Cache-Control')).toBe( + 'public, max-age=31536000, immutable' + ) + } catch (err) { + err.message = res.url + ' ' + err.message + throw err + } + }) + }) + + it('should set correct Cache-Control header for static 404s', async () => { + // this is to fix where 404 headers are set to 'public, max-age=31536000, immutable' + const res = await fetchViaHTTP( + `http://localhost:${next.appPort}`, + `/_next//static/common/bad-static.js` + ) + + expect(res.status).toBe(404) + expect(res.headers.get('Cache-Control')).toBe( + 'no-cache, no-store, max-age=0, must-revalidate' + ) + }) + + it('should block special pages', async () => { + const urls = ['/_document', '/_app'] + for (const url of urls) { + const html = await renderViaHTTP(next.appPort, url) + expect(html).toMatch(/404/) + } + }) + + it('should not contain customServer in NEXT_DATA', async () => { + const html = await renderViaHTTP(next.appPort, '/') + const $ = cheerio.load(html) + expect('customServer' in JSON.parse($('#__NEXT_DATA__').text())).toBe( + false + ) + }) + }) + + describe('API routes', () => { + it('should work with pages/api/index.js', async () => { + const url = `http://localhost:${next.appPort}` + const res = await fetchViaHTTP(url, `/api`) + const body = await res.text() + expect(body).toEqual('API index works') + }) + + it('should work with pages/api/hello.js', async () => { + const url = `http://localhost:${next.appPort}` + const res = await fetchViaHTTP(url, `/api/hello`) + const body = await res.text() + expect(body).toEqual('API hello works') + }) + + // Today, `__dirname` usage fails because Next.js moves the source file + // to .next/server/pages/api but it doesn't move the asset file. + // In the future, it would be nice to make `__dirname` work too. + it('does not work with pages/api/readfile-dirname.js', async () => { + const url = `http://localhost:${next.appPort}` + const res = await fetchViaHTTP(url, `/api/readfile-dirname`) + expect(res.status).toBe(500) + }) + + it('should work with pages/api/readfile-processcwd.js', async () => { + const url = `http://localhost:${next.appPort}` + const res = await fetchViaHTTP(url, `/api/readfile-processcwd`) + const body = await res.text() + expect(body).toBe('item') + }) + + it('should work with dynamic params and search string', async () => { + const url = `http://localhost:${next.appPort}` + const res = await fetchViaHTTP(url, `/api/post-1?val=1`) + const body = await res.json() + + expect(body).toEqual({ val: '1', post: 'post-1' }) + }) + }) + + describe('With navigation', () => { + it('should navigate via client side', async () => { + const browser = await webdriver(next.appPort, '/') + const text = await browser + .elementByCss('a') + .click() + .waitForElementByCss('.about-page') + .elementByCss('.about-page') + .text() + + expect(text).toBe('About Page') + await browser.close() + }) + + it('should navigate to nested index via client side', async () => { + const browser = await webdriver(next.appPort, '/another') + await browser.eval('window.beforeNav = 1') + + const text = await browser + .elementByCss('a') + .click() + .waitForElementByCss('.index-page') + .elementByCss('p') + .text() + + expect(text).toBe('Hello World') + expect(await browser.eval('window.beforeNav')).toBe(1) + await browser.close() + }) + + it('should reload page successfully (on bad link)', async () => { + const browser = await webdriver(next.appPort, '/to-nonexistent') + await browser.eval(function setup() { + // @ts-expect-error Exists on window + window.__DATA_BE_GONE = 'true' + }) + await browser.waitForElementByCss('#to-nonexistent-page') + await browser.click('#to-nonexistent-page') + await browser.waitForElementByCss('.about-page') + + const oldData = await browser.eval(`window.__DATA_BE_GONE`) + expect(oldData).toBeFalsy() + }) + + it('should reload page successfully (on bad data fetch)', async () => { + const browser = await webdriver(next.appPort, '/to-shadowed-page') + await browser.eval(function setup() { + // @ts-expect-error Exists on window + window.__DATA_BE_GONE = 'true' + }) + await browser.waitForElementByCss('#to-shadowed-page').click() + await browser.waitForElementByCss('.about-page') + + const oldData = await browser.eval(`window.__DATA_BE_GONE`) + expect(oldData).toBeFalsy() + }) + }) + + it('should navigate to external site and back', async () => { + const browser = await webdriver(next.appPort, '/external-and-back') + const initialText = await browser.elementByCss('p').text() + expect(initialText).toBe('server') + + await browser + .elementByCss('a') + .click() + .waitForElementByCss('input') + .back() + .waitForElementByCss('p') + + await waitFor(1000) + const newText = await browser.elementByCss('p').text() + expect(newText).toBe('server') + }) + + it('should navigate to page with CSS and back', async () => { + const browser = await webdriver(next.appPort, '/css-and-back') + const initialText = await browser.elementByCss('p').text() + expect(initialText).toBe('server') + + await browser + .elementByCss('a') + .click() + .waitForElementByCss('input') + .back() + .waitForElementByCss('p') + + await waitFor(1000) + const newText = await browser.elementByCss('p').text() + expect(newText).toBe('client') + }) + + it('should navigate to external site and back (with query)', async () => { + const browser = await webdriver( + next.appPort, + '/external-and-back?hello=world' + ) + const initialText = await browser.elementByCss('p').text() + expect(initialText).toBe('server') + + await browser + .elementByCss('a') + .click() + .waitForElementByCss('input') + .back() + .waitForElementByCss('p') + + await waitFor(1000) + const newText = await browser.elementByCss('p').text() + expect(newText).toBe('server') + }) + + it('should change query correctly', async () => { + const browser = await webdriver(next.appPort, '/query?id=0') + let id = await browser.elementByCss('#q0').text() + expect(id).toBe('0') + + await browser.elementByCss('#first').click().waitForElementByCss('#q1') + + id = await browser.elementByCss('#q1').text() + expect(id).toBe('1') + + await browser.elementByCss('#second').click().waitForElementByCss('#q2') + + id = await browser.elementByCss('#q2').text() + expect(id).toBe('2') + }) + + describe('Runtime errors', () => { + it('should render a server side error on the client side', async () => { + const browser = await webdriver(next.appPort, '/error-in-ssr-render') + await waitFor(2000) + const text = await browser.elementByCss('body').text() + // this makes sure we don't leak the actual error to the client side in production + expect(text).toMatch(/Internal Server Error\./) + const headingText = await browser.elementByCss('h1').text() + // This makes sure we render statusCode on the client side correctly + expect(headingText).toBe('500') + await browser.close() + }) + + it('should render a client side component error', async () => { + const browser = await webdriver( + next.appPort, + '/error-in-browser-render' + ) + await waitFor(2000) + const text = await browser.elementByCss('body').text() + expect(text).toMatch( + /Application error: a client-side exception has occurred/ + ) + await browser.close() + }) + + it('should call getInitialProps on _error page during a client side component error', async () => { + const browser = await webdriver( + next.appPort, + '/error-in-browser-render-status-code' + ) + await waitFor(2000) + const text = await browser.elementByCss('body').text() + expect(text).toMatch(/This page could not be found\./) + await browser.close() + }) + }) + + describe('Misc', () => { + it('should handle already finished responses', async () => { + const html = await renderViaHTTP(next.appPort, '/finish-response') + expect(html).toBe('hi') + }) + + it('should allow to access /static/ and /_next/', async () => { + // This is a test case which prevent the following issue happening again. + // See: https://github.com/vercel/next.js/issues/2617 + await renderViaHTTP(next.appPort, '/_next/') + await renderViaHTTP(next.appPort, '/static/') + const data = await renderViaHTTP(next.appPort, '/static/data/item.txt') + expect(data).toBe('item') + }) + + it('Should allow access to public files', async () => { + const data = await renderViaHTTP(next.appPort, '/data/data.txt') + const file = await renderViaHTTP(next.appPort, '/file') + const legacy = await renderViaHTTP(next.appPort, '/static/legacy.txt') + expect(data).toBe('data') + expect(file).toBe('test') + expect(legacy).toMatch(`new static folder`) + }) + + // TODO: do we want to normalize this for firefox? It seems in + // the latest version of firefox the window state is not reset + // when navigating back from a hard navigation. This might be + // a bug as other browsers do not behave this way. + if (global.browserName !== 'firefox') { + it('should reload the page on page script error', async () => { + const browser = await webdriver(next.appPort, '/counter') + const counter = await browser + .elementByCss('#increase') + .click() + .click() + .elementByCss('#counter') + .text() + expect(counter).toBe('Counter: 2') + + // When we go to the 404 page, it'll do a hard reload. + // So, it's possible for the front proxy to load a page from another zone. + // Since the page is reloaded, when we go back to the counter page again, + // previous counter value should be gone. + const counterAfter404Page = await browser + .elementByCss('#no-such-page') + .click() + .waitForElementByCss('h1') + .back() + .waitForElementByCss('#counter-page') + .elementByCss('#counter') + .text() + expect(counterAfter404Page).toBe('Counter: 0') + + await browser.close() + }) + } + + it('should have default runtime values when not defined', async () => { + const html = await renderViaHTTP(next.appPort, '/runtime-config') + expect(html).toMatch(/found public config/) + expect(html).toMatch(/found server config/) + }) + + it('should not have runtimeConfig in __NEXT_DATA__', async () => { + const html = await renderViaHTTP(next.appPort, '/runtime-config') + const $ = cheerio.load(html) + const script = $('#__NEXT_DATA__').html() + expect(script).not.toMatch(/runtimeConfig/) + }) + + it('should add autoExport for auto pre-rendered pages', async () => { + for (const page of ['/', '/about']) { + const html = await renderViaHTTP(next.appPort, page) + const $ = cheerio.load(html) + const data = JSON.parse($('#__NEXT_DATA__').html()) + expect(data.autoExport).toBe(true) + } + }) + + it('should not add autoExport for non pre-rendered pages', async () => { + for (const page of ['/query']) { + const html = await renderViaHTTP(next.appPort, page) + const $ = cheerio.load(html) + const data = JSON.parse($('#__NEXT_DATA__').html()) + expect(!!data.autoExport).toBe(false) + } + }) + + it('should add prefetch tags when Link prefetch prop is used', async () => { + const browser = await webdriver(next.appPort, '/prefetch') + + if (global.browserName === 'internet explorer') { + // IntersectionObserver isn't present so we need to trigger manually + await waitFor(1000) + await browser.eval(`(function() { + window.next.router.prefetch('/') + window.next.router.prefetch('/process-env') + window.next.router.prefetch('/counter') + window.next.router.prefetch('/about') + })()`) + } + + await waitFor(2000) + + if (global.browserName === 'safari') { + const elements = await browser.elementsByCss('link[rel=preload]') + // optimized preloading uses defer instead of preloading and prefetches + // aren't generated client-side since safari does not support prefetch + expect(elements.length).toBe(0) + } else { + const elements = await browser.elementsByCss('link[rel=prefetch]') + expect(elements.length).toBe(4) + + for (const element of elements) { + const rel = await element.getAttribute('rel') + const as = await element.getAttribute('as') + expect(rel).toBe('prefetch') + expect(as).toBe('script') + } + } + await browser.close() + }) + + // This is a workaround to fix https://github.com/vercel/next.js/issues/5860 + // TODO: remove this workaround when https://bugs.webkit.org/show_bug.cgi?id=187726 is fixed. + it('It does not add a timestamp to link tags with prefetch attribute', async () => { + const browser = await webdriver(next.appPort, '/prefetch') + const links = await browser.elementsByCss('link[rel=prefetch]') + + for (const element of links) { + const href = await element.getAttribute('href') + expect(href).not.toMatch(/\?ts=/) + } + const scripts = await browser.elementsByCss('script[src]') + + for (const element of scripts) { + const src = await element.getAttribute('src') + expect(src).not.toMatch(/\?ts=/) + } + await browser.close() + }) + + if (global.browserName === 'chrome') { + it('should reload the page on page script error with prefetch', async () => { + const browser = await webdriver(next.appPort, '/counter') + if (global.browserName !== 'chrome') return + const counter = await browser + .elementByCss('#increase') + .click() + .click() + .elementByCss('#counter') + .text() + expect(counter).toBe('Counter: 2') + + // Let the browser to prefetch the page and error it on the console. + await waitFor(3000) + + // When we go to the 404 page, it'll do a hard reload. + // So, it's possible for the front proxy to load a page from another zone. + // Since the page is reloaded, when we go back to the counter page again, + // previous counter value should be gone. + const counterAfter404Page = await browser + .elementByCss('#no-such-page-prefetch') + .click() + .waitForElementByCss('h1') + .back() + .waitForElementByCss('#counter-page') + .elementByCss('#counter') + .text() + expect(counterAfter404Page).toBe('Counter: 0') + + await browser.close() + }) + } + }) + + it('should not expose the compiled page file in development', async () => { + const url = `http://localhost:${next.appPort}` + await fetchViaHTTP(`${url}`, `/stateless`) // make sure the stateless page is built + const clientSideJsRes = await fetchViaHTTP( + `${url}`, + '/_next/development/static/development/pages/stateless.js' + ) + expect(clientSideJsRes.status).toBe(404) + const clientSideJsBody = await clientSideJsRes.text() + expect(clientSideJsBody).toMatch(/404/) + + const serverSideJsRes = await fetchViaHTTP( + `${url}`, + '/_next/development/server/static/development/pages/stateless.js' + ) + expect(serverSideJsRes.status).toBe(404) + const serverSideJsBody = await serverSideJsRes.text() + expect(serverSideJsBody).toMatch(/404/) + }) + + it('should not put backslashes in pages-manifest.json', () => { + // Whatever platform you build on, pages-manifest.json should use forward slash (/) + // See: https://github.com/vercel/next.js/issues/4920 + const pagesManifest = require(join( + next.testDir, + '.next', + 'server', + PAGES_MANIFEST + )) + + for (let key of Object.keys(pagesManifest)) { + expect(key).not.toMatch(/\\/) + expect(pagesManifest[key]).not.toMatch(/\\/) + } + }) + + it('should handle failed param decoding', async () => { + const html = await renderViaHTTP( + next.appPort, + '/invalid-param/%DE~%C7%1fY/' + ) + expect(html).toMatch(/400/) + expect(html).toMatch(/Bad Request/) + }) + + it('should replace static pages with HTML files', async () => { + const pages = ['/about', '/another', '/counter', '/dynamic', '/prefetch'] + for (const page of pages) { + const file = getPageFileFromPagesManifest(next.testDir, page) + + expect(file.endsWith('.html')).toBe(true) + } + }) + + it('should not replace non-static pages with HTML files', async () => { + const pages = ['/api', '/external-and-back', '/finish-response'] + + for (const page of pages) { + const file = getPageFileFromPagesManifest(next.testDir, page) + + expect(file.endsWith('.js')).toBe(true) + } + }) + + it('should handle AMP correctly in IE', async () => { + const browser = await webdriver(next.appPort, '/some-amp') + const text = await browser.elementByCss('p').text() + expect(text).toBe('Not AMP') + }) + + it('should warn when prefetch is true', async () => { + if (global.browserName !== 'chrome') return + let browser + try { + browser = await webdriver(next.appPort, '/development-logs') + const browserLogs = await browser.log('browser') + let found = false + browserLogs.forEach((log) => { + if (log.message.includes('Next.js auto-prefetches automatically')) { + found = true + } + }) + expect(found).toBe(false) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should not emit stats', async () => { + expect(existsSync(join(next.testDir, '.next', 'next-stats.json'))).toBe( + false + ) + }) + + it('should contain the Next.js version in window export', async () => { + let browser + try { + browser = await webdriver(next.appPort, '/about') + const version = await browser.eval('window.next.version') + expect(version).toBeTruthy() + expect(version).toBe( + (await next.readJSON('node_modules/next/package.json')).version + ) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should clear all core performance marks', async () => { + let browser + try { + browser = await webdriver(next.appPort, '/fully-dynamic') + + const currentPerfMarks = await browser.eval( + `window.performance.getEntriesByType('mark')` + ) + const allPerfMarks = [ + 'beforeRender', + 'afterHydrate', + 'afterRender', + 'routeChange', + ] + + allPerfMarks.forEach((name) => + expect(currentPerfMarks).not.toContainEqual( + expect.objectContaining({ name }) + ) + ) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should not clear custom performance marks', async () => { + let browser + try { + browser = await webdriver(next.appPort, '/mark-in-head') + + const customMarkFound = await browser.eval( + `window.performance.getEntriesByType('mark').filter(function(e) { + return e.name === 'custom-mark' + }).length === 1` + ) + expect(customMarkFound).toBe(true) + } finally { + if (browser) { + await browser.close() + } + } + }) + + it('should have defer on all script tags', async () => { + const html = await renderViaHTTP(next.appPort, '/') + const $ = cheerio.load(html) + let missing = false + + for (const script of $('script').toArray()) { + // application/json doesn't need async + if ( + script.attribs.type === 'application/json' || + script.attribs.src.includes('polyfills') + ) { + continue + } + + if (script.attribs.defer !== '' || script.attribs.async === '') { + missing = true + } + } + expect(missing).toBe(false) + }) + + it('should only have one DOCTYPE', async () => { + const html = await renderViaHTTP(next.appPort, '/') + expect(html).toMatch(/^ { + const browser = await webdriver(next.appPort, '/') + await browser.eval(`(function() { + window.beforeNav = 1 + window.next.router.push({ + pathname: '/non-existent', + query: { hello: 'world' } + }) + })()`) + + await check( + () => browser.eval('document.documentElement.innerHTML'), + /page could not be found/ + ) + + expect(await browser.eval('window.beforeNav')).toBeFalsy() + expect(await browser.eval('window.location.hash')).toBe('') + expect(await browser.eval('window.location.search')).toBe( + '?hello=world' + ) + expect(await browser.eval('window.location.pathname')).toBe( + '/non-existent' + ) + }) + } + + it('should remove placeholder for next/image correctly', async () => { + const browser = await webdriver(next.appPort, '/') + + await browser.eval(`(function() { + window.beforeNav = 1 + window.next.router.push('/static-image') + })()`) + await browser.waitForElementByCss('#static-image') + + expect(await browser.eval('window.beforeNav')).toBe(1) + + await check( + () => browser.elementByCss('img').getComputedCss('background-image'), + 'none' + ) + + await browser.eval(`(function() { + window.beforeNav = 1 + window.next.router.push('/') + })()`) + await browser.waitForElementByCss('.index-page') + await waitFor(1000) + + await browser.eval(`(function() { + window.beforeNav = 1 + window.next.router.push('/static-image') + })()`) + await browser.waitForElementByCss('#static-image') + + expect(await browser.eval('window.beforeNav')).toBe(1) + + await check( + () => + browser + .elementByCss('#static-image') + .getComputedCss('background-image'), + 'none' + ) + + for (let i = 0; i < 5; i++) { + expect( + await browser + .elementByCss('#static-image') + .getComputedCss('background-image') + ).toBe('none') + await waitFor(500) + } + }) + + dynamicImportTests(next, (p, q) => renderViaHTTP(next.appPort, p, q)) + + processEnv(next) + if (global.browserName !== 'safari') security(next) + } +) diff --git a/test/integration/production/test/process-env.js b/test/production/pages-dir/production/test/process-env.ts similarity index 85% rename from test/integration/production/test/process-env.js rename to test/production/pages-dir/production/test/process-env.ts index 00d4aaa20f..65d75c0d0e 100644 --- a/test/integration/production/test/process-env.js +++ b/test/production/pages-dir/production/test/process-env.ts @@ -1,17 +1,15 @@ /* eslint-env jest */ import webdriver from 'next-webdriver' -import { join } from 'path' import { readNextBuildClientPageFile, readNextBuildServerPageFile, } from 'next-test-utils' +import { NextInstance } from 'e2e-utils' -const appDir = join(__dirname, '..') - -export default (context) => { +export default (next: NextInstance) => { describe('process.env', () => { it('should set process.env.NODE_ENV in production', async () => { - const browser = await webdriver(context.appPort, '/process-env') + const browser = await webdriver(next.appPort, '/process-env') const nodeEnv = await browser.elementByCss('#node-env').text() expect(nodeEnv).toBe('production') await browser.close() @@ -21,7 +19,7 @@ export default (context) => { describe('process.browser', () => { it('should eliminate server only code on the client', async () => { const clientCode = await readNextBuildClientPageFile( - appDir, + next.testDir, '/process-env' ) expect(clientCode).toMatch( @@ -34,7 +32,7 @@ export default (context) => { it('should eliminate client only code on the server', async () => { const serverCode = await readNextBuildServerPageFile( - appDir, + next.testDir, '/process-env' ) expect(serverCode).not.toMatch( diff --git a/test/integration/production/test/security.js b/test/production/pages-dir/production/test/security.ts similarity index 91% rename from test/integration/production/test/security.js rename to test/production/pages-dir/production/test/security.ts index edb1692f0d..779f8270ec 100644 --- a/test/integration/production/test/security.js +++ b/test/production/pages-dir/production/test/security.ts @@ -1,5 +1,4 @@ /* eslint-env jest */ -/* global browserName */ import webdriver from 'next-webdriver' import { readFileSync } from 'fs' import http from 'http' @@ -8,6 +7,7 @@ import { join } from 'path' import { getBrowserBodyText, waitFor, fetchViaHTTP } from 'next-test-utils' import { recursiveReadDir } from 'next/dist/lib/recursive-readdir' import { homedir } from 'os' +import { NextInstance } from 'e2e-utils' // Does the same evaluation checking for INJECTED for 5 seconds after hydration, triggering every 500ms async function checkInjected(browser) { @@ -21,7 +21,7 @@ async function checkInjected(browser) { } } -module.exports = (context) => { +export default (next: NextInstance) => { describe('With Security Related Issues', () => { it.skip('should handle invalid URL properly', async () => { async function invalidRequest() { @@ -29,7 +29,7 @@ module.exports = (context) => { const request = http.request( { hostname: `localhost`, - port: context.appPort, + port: next.appPort, path: `*`, }, (response) => { @@ -50,7 +50,7 @@ module.exports = (context) => { }) it('should only access files inside .next directory', async () => { - const buildId = readFileSync(join(__dirname, '../.next/BUILD_ID'), 'utf8') + const buildId = next.buildId const pathsToCheck = [ `/_next/${buildId}/page/../../../info`, @@ -68,7 +68,7 @@ module.exports = (context) => { ] for (const path of pathsToCheck) { - const res = await fetchViaHTTP(context.appPort, path) + const res = await fetchViaHTTP(next.appPort, path) const data = await res.text() expect(data.includes('cool-version')).toBeFalsy() expect([400, 404].includes(res.status)).toBeTruthy() @@ -83,7 +83,7 @@ module.exports = (context) => { `/_next/static/../routes-manifest.json`, ] for (const path of pathsToCheck) { - const res = await fetchViaHTTP(context.appPort, path) + const res = await fetchViaHTTP(next.appPort, path) const text = await res.text() try { expect(res.status).toBe(404) @@ -95,9 +95,9 @@ module.exports = (context) => { }) it("should not leak the user's home directory into the build", async () => { - const buildId = readFileSync(join(__dirname, '../.next/BUILD_ID'), 'utf8') + const buildId = next.buildId - const readPath = join(__dirname, `../.next/static/${buildId}`) + const readPath = join(next.testDir, `.next/static/${buildId}`) const buildFiles = await recursiveReadDir(readPath, { pathnameFilter: (f) => /\.js$/.test(f), }) @@ -131,7 +131,7 @@ module.exports = (context) => { it('should prevent URI based XSS attacks', async () => { const browser = await webdriver( - context.appPort, + next.appPort, '/\',document.body.innerHTML="INJECTED",\'' ) await checkInjected(browser) @@ -140,7 +140,7 @@ module.exports = (context) => { it('should prevent URI based XSS attacks using single quotes', async () => { const browser = await webdriver( - context.appPort, + next.appPort, `/'-(document.body.innerHTML='INJECTED')-'` ) await checkInjected(browser) @@ -149,7 +149,7 @@ module.exports = (context) => { it('should prevent URI based XSS attacks using double quotes', async () => { const browser = await webdriver( - context.appPort, + next.appPort, `/"-(document.body.innerHTML='INJECTED')-"` ) await checkInjected(browser) @@ -159,7 +159,7 @@ module.exports = (context) => { it('should prevent URI based XSS attacks using semicolons and double quotes', async () => { const browser = await webdriver( - context.appPort, + next.appPort, `/;"-(document.body.innerHTML='INJECTED')-"` ) await checkInjected(browser) @@ -169,7 +169,7 @@ module.exports = (context) => { it('should prevent URI based XSS attacks using semicolons and single quotes', async () => { const browser = await webdriver( - context.appPort, + next.appPort, `/;'-(document.body.innerHTML='INJECTED')-'` ) await checkInjected(browser) @@ -179,7 +179,7 @@ module.exports = (context) => { it('should prevent URI based XSS attacks using src', async () => { const browser = await webdriver( - context.appPort, + next.appPort, `/javascript:(document.body.innerHTML='INJECTED')` ) await checkInjected(browser) @@ -189,7 +189,7 @@ module.exports = (context) => { it('should prevent URI based XSS attacks using querystring', async () => { const browser = await webdriver( - context.appPort, + next.appPort, `/?javascript=(document.body.innerHTML='INJECTED')` ) await checkInjected(browser) @@ -199,7 +199,7 @@ module.exports = (context) => { it('should prevent URI based XSS attacks using querystring and quotes', async () => { const browser = await webdriver( - context.appPort, + next.appPort, `/?javascript="(document.body.innerHTML='INJECTED')"` ) await checkInjected(browser) @@ -208,7 +208,7 @@ module.exports = (context) => { it('should handle encoded value in the pathname correctly \\', async () => { const res = await fetchViaHTTP( - context.appPort, + next.appPort, '/redirect/me/to-about/' + encodeURI('\\google.com'), undefined, { @@ -226,7 +226,7 @@ module.exports = (context) => { it('should handle encoded value in the pathname correctly %', async () => { const res = await fetchViaHTTP( - context.appPort, + next.appPort, '/redirect/me/to-about/%25google.com', undefined, { @@ -244,7 +244,7 @@ module.exports = (context) => { it('should handle encoded value in the query correctly', async () => { const res = await fetchViaHTTP( - context.appPort, + next.appPort, '/trailing-redirect/?url=https%3A%2F%2Fgoogle.com%2Fimage%3Fcrop%3Dfocalpoint%26w%3D24&w=1200&q=100', undefined, { @@ -265,7 +265,7 @@ module.exports = (context) => { it('should handle encoded value in the pathname correctly /', async () => { const res = await fetchViaHTTP( - context.appPort, + next.appPort, '/redirect/me/to-about/%2fgoogle.com', undefined, { @@ -283,7 +283,7 @@ module.exports = (context) => { it('should handle encoded value in the pathname to query correctly (/)', async () => { const res = await fetchViaHTTP( - context.appPort, + next.appPort, '/redirect-query-test/%2Fgoogle.com', undefined, { @@ -303,7 +303,7 @@ module.exports = (context) => { it('should handle encoded / value for trailing slash correctly', async () => { const res = await fetchViaHTTP( - context.appPort, + next.appPort, '/%2fexample.com/', undefined, { redirect: 'manual' } @@ -317,17 +317,17 @@ module.exports = (context) => { expect(hostname).not.toBe('example.com') }) - if (browserName !== 'internet explorer') { + if (global.browserName !== 'internet explorer') { it('should not execute script embedded inside svg image, even if dangerouslyAllowSVG=true', async () => { let browser try { - browser = await webdriver(context.appPort, '/svg-image') + browser = await webdriver(next.appPort, '/svg-image') await browser.eval(`document.getElementById("img").scrollIntoView()`) const src = await browser.elementById('img').getAttribute('src') expect(src).toMatch(/_next\/image\?.*xss\.svg/) expect(await browser.elementById('msg').text()).toBe('safe') browser = await webdriver( - context.appPort, + next.appPort, '/_next/image?url=%2Fxss.svg&w=256&q=75' ) expect(await browser.elementById('msg').text()).toBe('safe')