diff --git a/.github/workflows/e2e-next.yml b/.github/workflows/e2e-next.yml
index 4244beac2a..b435fe4119 100644
--- a/.github/workflows/e2e-next.yml
+++ b/.github/workflows/e2e-next.yml
@@ -6,19 +6,77 @@ on:
branches: [main]
jobs:
+ setup:
+ runs-on: ubuntu-latest
+ outputs:
+ test-files: ${{ steps['set-test-files'].outputs['test-files'] }}
+ steps:
+ - uses: actions/checkout@v3
+ - run: npm install
+ - id: set-test-files
+ name: Get test files
+ # Extracts the list of all test files as JSON and trims to be relative to the test dir to be easier to read
+ run:
+ echo "test-files=$(npx jest -c test/e2e/jest.config.js --listTests --json | jq -cM 'map(.[env.PWD | length +
+ 10:])')" >> $GITHUB_OUTPUT
+ # echo "test-files=$(npx jest -c test/e2e/jest.config.all.js --listTests --json | jq -cM 'map(.[env.PWD | length
+ # + 10:])')" >> $GITHUB_OUTPUT
+
test:
- name: E2E tests
runs-on: ubuntu-latest
+ name: test (${{ matrix.test-file }})
+ needs:
+ - setup
+ strategy:
+ fail-fast: false
+ matrix:
+ # Creates a job for each chunk ID. This will be assigned one or more test files to run
+ test-file: ${{ fromJson(needs.setup.outputs['test-files']) }}
steps:
- - uses: actions/checkout@v2
- - name: Installing with LTS Node.js
- uses: actions/setup-node@v2
- with:
- node-version: 'lts/*'
- - name: NPM Install
- run: npm install
- - name: Run Next.js e2e test suite
- run: npm run test:next
+ - uses: actions/checkout@v3
+ - run: npm install
+ - name: Run tests
+ run: npx jest --reporters=jest-junit --reporters=default -c test/e2e/jest.config.all.js ${{ matrix.test-file }}
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_BOT_AUTH_TOKEN }}
NETLIFY_SITE_ID: 1d5a5c76-d445-4ae5-b694-b0d3f2e2c395
+ # RUN_SKIPPED_TESTS: true
+ # - uses: phoenix-actions/test-reporting@v10
+ # if: ${{ always() }}
+ # name: Report Test Results
+ # # Generates annotations for the test failures
+ # id: test-report
+ # with:
+ # name: E2E Test chunk ${{ matrix.chunk }}
+ # path: 'reports/**/*.xml' # Path to test results (inside artifact .zip)
+ # output-to: 'checks'
+ # max-annotations: 49 # Maximum number of annotations to be created
+ # reporter: jest-junit # Format of test results
+
+ - uses: actions/upload-artifact@v3
+ # upload test results
+ if: ${{ always() }}
+ name: Upload test results
+ with:
+ name: test-results
+ path: reports/jest-*.xml
+ report:
+ runs-on: ubuntu-latest
+ if: ${{ always() }}
+ needs:
+ - test
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/download-artifact@v3
+ with:
+ path: reports
+ - name: Combine reports
+ # The test reporter can handle multiple files, but these have random filenames so the output is better when combined
+ run: npx junit-report-merger test-results.xml reports/**/*.xml
+ - uses: phoenix-actions/test-reporting@v10
+ with:
+ name: Jest Tests
+ output-to: 'step-summary'
+ path: 'test-results.xml'
+ max-annotations: 49
+ reporter: jest-junit
diff --git a/.gitignore b/.gitignore
index 37fa1f60e3..97143df73e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -150,3 +150,6 @@ packages/*/lib
# Cypress
cypress/screenshots
+
+# Test cases have node module fixtures
+!test/**/node_modules
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index dbd6465334..1fed9d5f8f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -50,6 +50,7 @@
"husky": "^7.0.4",
"jest": "^27.0.0",
"jest-fetch-mock": "^3.0.3",
+ "jest-junit": "^14.0.1",
"netlify-plugin-cypress": "^2.2.0",
"npm-run-all": "^4.1.5",
"playwright-chromium": "^1.26.1",
@@ -4356,40 +4357,6 @@
"node": ">=16.0.0"
}
},
- "node_modules/@netlify/eslint-config-node/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/@netlify/eslint-config-node/node_modules/eslint-plugin-n": {
- "version": "14.0.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-14.0.0.tgz",
- "integrity": "sha512-mNwplPLsbaKhHyA0fa/cy8j+oF6bF6l81hzBTWa6JOvPcMNAuIogk2ih6d9tYvWYzyUG+7ZFeChqbzdFpg2QrQ==",
- "dev": true,
- "dependencies": {
- "eslint-plugin-es": "^4.1.0",
- "eslint-utils": "^3.0.0",
- "ignore": "^5.1.1",
- "is-core-module": "^2.3.0",
- "minimatch": "^3.0.4",
- "resolve": "^1.10.1",
- "semver": "^6.1.0"
- },
- "engines": {
- "node": ">=12.22.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/mysticatea"
- },
- "peerDependencies": {
- "eslint": ">=7.0.0"
- }
- },
"node_modules/@netlify/eslint-config-node/node_modules/execa": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz",
@@ -4449,18 +4416,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/@netlify/eslint-config-node/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/@netlify/eslint-config-node/node_modules/npm-run-path": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
@@ -4491,23 +4446,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/@netlify/eslint-config-node/node_modules/resolve": {
- "version": "1.22.1",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
- "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
- "dev": true,
- "dependencies": {
- "is-core-module": "^2.9.0",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- },
- "bin": {
- "resolve": "bin/resolve"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/@netlify/eslint-config-node/node_modules/strip-final-newline": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
@@ -5741,13 +5679,13 @@
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
- "devOptional": true
+ "dev": true
},
"node_modules/@types/react": {
"version": "18.0.25",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.25.tgz",
"integrity": "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==",
- "devOptional": true,
+ "dev": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -5773,7 +5711,7 @@
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
- "devOptional": true
+ "dev": true
},
"node_modules/@types/semver": {
"version": "7.3.13",
@@ -9608,7 +9546,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
"integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==",
- "devOptional": true
+ "dev": true
},
"node_modules/custom-routes": {
"resolved": "demos/custom-routes",
@@ -11493,20 +11431,18 @@
}
},
"node_modules/eslint-plugin-n": {
- "version": "15.5.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.5.1.tgz",
- "integrity": "sha512-kAd+xhZm7brHoFLzKLB7/FGRFJNg/srmv67mqb7tto22rpr4wv/LV6RuXzAfv3jbab7+k1wi42PsIhGviywaaw==",
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-14.0.0.tgz",
+ "integrity": "sha512-mNwplPLsbaKhHyA0fa/cy8j+oF6bF6l81hzBTWa6JOvPcMNAuIogk2ih6d9tYvWYzyUG+7ZFeChqbzdFpg2QrQ==",
"dev": true,
- "peer": true,
"dependencies": {
- "builtins": "^5.0.1",
"eslint-plugin-es": "^4.1.0",
"eslint-utils": "^3.0.0",
"ignore": "^5.1.1",
- "is-core-module": "^2.11.0",
- "minimatch": "^3.1.2",
- "resolve": "^1.22.1",
- "semver": "^7.3.8"
+ "is-core-module": "^2.3.0",
+ "minimatch": "^3.0.4",
+ "resolve": "^1.10.1",
+ "semver": "^6.1.0"
},
"engines": {
"node": ">=12.22.0"
@@ -11523,7 +11459,6 @@
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
- "peer": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -11534,7 +11469,6 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
- "peer": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -11547,7 +11481,6 @@
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
"dev": true,
- "peer": true,
"dependencies": {
"is-core-module": "^2.9.0",
"path-parse": "^1.0.7",
@@ -11560,22 +11493,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/eslint-plugin-n/node_modules/semver": {
- "version": "7.3.8",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
- "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/eslint-plugin-promise": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz",
@@ -13904,7 +13821,7 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
"integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
- "devOptional": true
+ "dev": true
},
"node_modules/import-fresh": {
"version": "3.3.0",
@@ -15322,6 +15239,33 @@
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
+ "node_modules/jest-junit": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-14.0.1.tgz",
+ "integrity": "sha512-h7/wwzPbllgpQhhVcRzRC76/cc89GlazThoV1fDxcALkf26IIlRsu/AcTG64f4nR2WPE3Cbd+i/sVf+NCUHrWQ==",
+ "dev": true,
+ "dependencies": {
+ "mkdirp": "^1.0.4",
+ "strip-ansi": "^6.0.1",
+ "uuid": "^8.3.2",
+ "xml": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/jest-junit/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/jest-leak-detector": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz",
@@ -21461,7 +21405,7 @@
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.56.1.tgz",
"integrity": "sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ==",
- "devOptional": true,
+ "dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
@@ -24307,6 +24251,12 @@
"node": ">=8"
}
},
+ "node_modules/xml": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
+ "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
+ "dev": true
+ },
"node_modules/xml-name-validator": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
@@ -27410,31 +27360,6 @@
"statuses": "^2.0.1"
},
"dependencies": {
- "brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "requires": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "eslint-plugin-n": {
- "version": "14.0.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-14.0.0.tgz",
- "integrity": "sha512-mNwplPLsbaKhHyA0fa/cy8j+oF6bF6l81hzBTWa6JOvPcMNAuIogk2ih6d9tYvWYzyUG+7ZFeChqbzdFpg2QrQ==",
- "dev": true,
- "requires": {
- "eslint-plugin-es": "^4.1.0",
- "eslint-utils": "^3.0.0",
- "ignore": "^5.1.1",
- "is-core-module": "^2.3.0",
- "minimatch": "^3.0.4",
- "resolve": "^1.10.1",
- "semver": "^6.1.0"
- }
- },
"execa": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz",
@@ -27470,15 +27395,6 @@
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
"dev": true
},
- "minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "requires": {
- "brace-expansion": "^1.1.7"
- }
- },
"npm-run-path": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
@@ -27497,17 +27413,6 @@
"mimic-fn": "^4.0.0"
}
},
- "resolve": {
- "version": "1.22.1",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
- "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
- "dev": true,
- "requires": {
- "is-core-module": "^2.9.0",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- }
- },
"strip-final-newline": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
@@ -28451,13 +28356,13 @@
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
- "devOptional": true
+ "dev": true
},
"@types/react": {
"version": "18.0.25",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.25.tgz",
"integrity": "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==",
- "devOptional": true,
+ "dev": true,
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -28483,7 +28388,7 @@
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
- "devOptional": true
+ "dev": true
},
"@types/semver": {
"version": "7.3.13",
@@ -28803,8 +28708,7 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"acorn-walk": {
"version": "7.2.0",
@@ -28861,8 +28765,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz",
"integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"ansi-align": {
"version": "3.0.1",
@@ -30906,8 +30809,7 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.2.0.tgz",
"integrity": "sha512-NkANeMnaHrlaSSlpKGyvn2R4rqUDeE/9E5YHx+b4nwo0R8dZyAqcih8/gxpCZvqWP9Vf6xuLpMSzSgdVEIM78g==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"cp-file": {
"version": "9.1.0",
@@ -31451,7 +31353,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
"integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==",
- "devOptional": true
+ "dev": true
},
"custom-routes": {
"version": "file:demos/custom-routes",
@@ -32636,15 +32538,13 @@
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz",
"integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"eslint-config-standard": {
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.0.0.tgz",
"integrity": "sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"eslint-formatter-codeframe": {
"version": "7.32.1",
@@ -33030,20 +32930,18 @@
}
},
"eslint-plugin-n": {
- "version": "15.5.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.5.1.tgz",
- "integrity": "sha512-kAd+xhZm7brHoFLzKLB7/FGRFJNg/srmv67mqb7tto22rpr4wv/LV6RuXzAfv3jbab7+k1wi42PsIhGviywaaw==",
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-14.0.0.tgz",
+ "integrity": "sha512-mNwplPLsbaKhHyA0fa/cy8j+oF6bF6l81hzBTWa6JOvPcMNAuIogk2ih6d9tYvWYzyUG+7ZFeChqbzdFpg2QrQ==",
"dev": true,
- "peer": true,
"requires": {
- "builtins": "^5.0.1",
"eslint-plugin-es": "^4.1.0",
"eslint-utils": "^3.0.0",
"ignore": "^5.1.1",
- "is-core-module": "^2.11.0",
- "minimatch": "^3.1.2",
- "resolve": "^1.22.1",
- "semver": "^7.3.8"
+ "is-core-module": "^2.3.0",
+ "minimatch": "^3.0.4",
+ "resolve": "^1.10.1",
+ "semver": "^6.1.0"
},
"dependencies": {
"brace-expansion": {
@@ -33051,7 +32949,6 @@
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
- "peer": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -33062,7 +32959,6 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
- "peer": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -33072,22 +32968,11 @@
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
"dev": true,
- "peer": true,
"requires": {
"is-core-module": "^2.9.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
}
- },
- "semver": {
- "version": "7.3.8",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
- "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
- "dev": true,
- "peer": true,
- "requires": {
- "lru-cache": "^6.0.0"
- }
}
}
},
@@ -33095,8 +32980,7 @@
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz",
"integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"eslint-plugin-react": {
"version": "7.31.10",
@@ -33154,8 +33038,7 @@
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
"integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"eslint-plugin-unicorn": {
"version": "43.0.2",
@@ -34705,7 +34588,7 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
"integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
- "devOptional": true
+ "dev": true
},
"import-fresh": {
"version": "3.3.0",
@@ -35732,6 +35615,29 @@
"throat": "^6.0.1"
}
},
+ "jest-junit": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-14.0.1.tgz",
+ "integrity": "sha512-h7/wwzPbllgpQhhVcRzRC76/cc89GlazThoV1fDxcALkf26IIlRsu/AcTG64f4nR2WPE3Cbd+i/sVf+NCUHrWQ==",
+ "dev": true,
+ "requires": {
+ "mkdirp": "^1.0.4",
+ "strip-ansi": "^6.0.1",
+ "uuid": "^8.3.2",
+ "xml": "^1.0.1"
+ },
+ "dependencies": {
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ }
+ }
+ },
"jest-leak-detector": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz",
@@ -35785,8 +35691,7 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
"integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"jest-regex-util": {
"version": "27.5.1",
@@ -40388,7 +40293,7 @@
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.56.1.tgz",
"integrity": "sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ==",
- "devOptional": true,
+ "dev": true,
"requires": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
@@ -41340,8 +41245,7 @@
"styled-jsx": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.7.tgz",
- "integrity": "sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==",
- "requires": {}
+ "integrity": "sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA=="
},
"supports-color": {
"version": "9.2.3",
@@ -42102,8 +42006,7 @@
"ws": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
- "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
- "requires": {}
+ "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg=="
}
}
},
@@ -42231,8 +42134,7 @@
"use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
- "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
- "requires": {}
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA=="
},
"util-deprecate": {
"version": "1.0.2",
@@ -42596,8 +42498,7 @@
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"xdg-basedir": {
"version": "4.0.0",
@@ -42605,6 +42506,12 @@
"integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==",
"dev": true
},
+ "xml": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
+ "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
+ "dev": true
+ },
"xml-name-validator": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
diff --git a/package.json b/package.json
index f5297ffa9d..36e66a3c4f 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,8 @@
"install-husky": "if-env CI=1 || husky install node_modules/@netlify/eslint-config-node/.husky",
"test": "run-s build:demo test:jest",
"test:next": "jest -c test/e2e/jest.config.js",
+ "test:next:disabled": "RUN_SKIPPED_TESTS=1 jest -c test/e2e/jest.config.disabled.js",
+ "test:next:all": "RUN_SKIPPED_TESTS=1 jest -c test/e2e/jest.config.all.js",
"test:jest": "jest",
"playwright:install": "playwright install --with-deps chromium",
"test:jest:update": "jest --updateSnapshot",
@@ -69,6 +71,7 @@
"husky": "^7.0.4",
"jest": "^27.0.0",
"jest-fetch-mock": "^3.0.3",
+ "jest-junit": "^14.0.1",
"netlify-plugin-cypress": "^2.2.0",
"npm-run-all": "^4.1.5",
"playwright-chromium": "^1.26.1",
@@ -104,6 +107,15 @@
"verbose": true,
"testTimeout": 60000
},
+ "jest-junit": {
+ "outputDirectory": "reports",
+ "outputName": "jest-junit.xml",
+ "uniqueOutputName": "true",
+ "ancestorSeparator": " > ",
+ "suiteNameTemplate": "{filepath}",
+ "classNameTemplate": "{classname}",
+ "titleTemplate": "{title}"
+ },
"workspaces": [
"packages/*",
"demos/canary",
diff --git a/test/e2e/README.md b/test/e2e/README.md
new file mode 100644
index 0000000000..84a57880c6
--- /dev/null
+++ b/test/e2e/README.md
@@ -0,0 +1,34 @@
+# Next.js e2e test suite
+
+The tests in this directory are taken from the Next.js monorepo.
+[See the licence](https://github.com/vercel/next.js/blob/canary/license.md). The files in `tests` run unmodofied. The
+ones in `modified-tests` either have fixes to run correctly, or have tests disabled. The tests in `disabled-tests` are
+current disabled because of either an incompatibility or bug.
+
+The tools in `next-test-lib` are based on the equivalent tools in the Next.js monorepo. The original utilities test the
+Next.js code, whereas here they are used to test the Netlify runtime. For this reason only the "deploy" mode is enabled,
+and it builds using the runtime packages in this monorepo and deploys using the Netlify CLI. The e2e tests then run
+against the deployed site.
+
+The script that runs these is in
+[.github/workflows/e2e-next.yml](https://github.com/netlify/next-runtime/blob/main/.github/workflows/e2e-next.yml). It
+runs all the tests in parallel using a matrix, with each running in a separate job. The tests are run against the
+version of the Next Runtime that is currently in the monorepo, and the version of `next` that is in the monorepo's
+dependencies.
+
+To run the tests locally, the npm script `test:next` can be used, which runs the default tests. The script
+`test:next:all` runs all the tests, including the ones in `disabled-tests` and the other tests skipped by Jest. The
+script `test:next:disabled` runs only the disabled tests. Unlike in CI, these are run sequentially, so will take a long
+time to run, so you may want to just run a single test suite. To run a single suite run
+`npm run test:next:all -- the-test-name-here`, e.g. `npm run test:next:all -- streaming-ssr`. The name is matched as a
+pattern against the path of the test file and can be a partial match.
+
+When they are run, the tests generate the sites in a temporary directory, and then deploy them to Netlify. The e2e tests
+are then run against these. This can be overridden by setting the env var `SITE_URL`, which will be used instead of
+deploying the test site. This only makes sense when running a single test suite, because each test suite runs against a
+different site.
+
+The tmp directory with each test site is deleted after the tests have been run. If you want to manually build or deploy
+the test sites, you can run the test command, and then you can kill the test process after it has generated the site.
+The location of the tmp directory is printed in the console. Alternatively, set the env var `NEXT_TEST_SKIP_CLEANUP` and
+the site directories will all be retained.
diff --git a/test/e2e/disabled-tests/edge-render-getserversideprops/app/next.config.js b/test/e2e/disabled-tests/edge-render-getserversideprops/app/next.config.js
new file mode 100644
index 0000000000..abad3c69e6
--- /dev/null
+++ b/test/e2e/disabled-tests/edge-render-getserversideprops/app/next.config.js
@@ -0,0 +1,14 @@
+module.exports = {
+ rewrites() {
+ return [
+ {
+ source: '/rewrite-me',
+ destination: '/',
+ },
+ {
+ source: '/rewrite-me-dynamic',
+ destination: '/first',
+ },
+ ]
+ },
+}
diff --git a/test/e2e/disabled-tests/edge-render-getserversideprops/app/pages/[id].js b/test/e2e/disabled-tests/edge-render-getserversideprops/app/pages/[id].js
new file mode 100644
index 0000000000..5d17ce2cb2
--- /dev/null
+++ b/test/e2e/disabled-tests/edge-render-getserversideprops/app/pages/[id].js
@@ -0,0 +1,23 @@
+export const config = {
+ runtime: 'experimental-edge',
+}
+
+export default function Page(props) {
+ return (
+ <>
+
/[id]
+ {JSON.stringify(props)}
+ >
+ )
+}
+
+export function getServerSideProps({ req, params, query }) {
+ return {
+ props: {
+ url: req.url,
+ query,
+ params,
+ now: Date.now(),
+ },
+ }
+}
diff --git a/test/e2e/disabled-tests/edge-render-getserversideprops/app/pages/index.js b/test/e2e/disabled-tests/edge-render-getserversideprops/app/pages/index.js
new file mode 100644
index 0000000000..c2380c49cc
--- /dev/null
+++ b/test/e2e/disabled-tests/edge-render-getserversideprops/app/pages/index.js
@@ -0,0 +1,23 @@
+export const config = {
+ runtime: 'experimental-edge',
+}
+
+export default function Page(props) {
+ return (
+ <>
+ /index
+ {JSON.stringify(props)}
+ >
+ )
+}
+
+export function getServerSideProps({ req, params, query }) {
+ return {
+ props: {
+ url: req.url,
+ query,
+ now: Date.now(),
+ params: params || null,
+ },
+ }
+}
diff --git a/test/e2e/disabled-tests/edge-render-getserversideprops/index.test.ts b/test/e2e/disabled-tests/edge-render-getserversideprops/index.test.ts
new file mode 100644
index 0000000000..c3ea23aca7
--- /dev/null
+++ b/test/e2e/disabled-tests/edge-render-getserversideprops/index.test.ts
@@ -0,0 +1,129 @@
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { fetchViaHTTP, normalizeRegEx, renderViaHTTP } from 'next-test-utils'
+import cheerio from 'cheerio'
+import { join } from 'path'
+import escapeStringRegexp from 'escape-string-regexp'
+
+describe('edge-render-getserversideprops', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: new FileRef(join(__dirname, 'app')),
+ dependencies: {},
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('should have correct query/params on index', async () => {
+ const html = await renderViaHTTP(next.url, '/')
+ const $ = cheerio.load(html)
+ expect($('#page').text()).toBe('/index')
+ const props = JSON.parse($('#props').text())
+ expect(props.query).toEqual({})
+ expect(props.params).toBe(null)
+ expect(props.url).toBe('/')
+ })
+
+ it('should have correct query/params on /[id]', async () => {
+ const html = await renderViaHTTP(next.url, '/123', { hello: 'world' })
+ const $ = cheerio.load(html)
+ expect($('#page').text()).toBe('/[id]')
+ const props = JSON.parse($('#props').text())
+ expect(props.query).toEqual({ id: '123', hello: 'world' })
+ expect(props.params).toEqual({ id: '123' })
+ expect(props.url).toBe('/123?hello=world')
+ })
+
+ it('should have correct query/params on rewrite', async () => {
+ const html = await renderViaHTTP(next.url, '/rewrite-me', {
+ hello: 'world',
+ })
+ const $ = cheerio.load(html)
+ expect($('#page').text()).toBe('/index')
+ const props = JSON.parse($('#props').text())
+ expect(props.query).toEqual({ hello: 'world' })
+ expect(props.params).toEqual(null)
+ expect(props.url).toBe('/rewrite-me?hello=world')
+ })
+
+ it('should have correct query/params on dynamic rewrite', async () => {
+ const html = await renderViaHTTP(next.url, '/rewrite-me-dynamic', {
+ hello: 'world',
+ })
+ const $ = cheerio.load(html)
+ expect($('#page').text()).toBe('/[id]')
+ const props = JSON.parse($('#props').text())
+ expect(props.query).toEqual({ id: 'first', hello: 'world' })
+ expect(props.params).toEqual({ id: 'first' })
+ expect(props.url).toBe('/rewrite-me-dynamic?hello=world')
+ })
+
+ it('should respond to _next/data for index correctly', async () => {
+ const res = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/index.json`,
+ undefined,
+ {
+ headers: {
+ 'x-nextjs-data': '1',
+ },
+ }
+ )
+ expect(res.status).toBe(200)
+ const { pageProps: props } = await res.json()
+ expect(props.query).toEqual({})
+ expect(props.params).toBe(null)
+ })
+
+ it('should respond to _next/data for [id] correctly', async () => {
+ const res = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/321.json`,
+ { hello: 'world' },
+ {
+ headers: {
+ 'x-nextjs-data': '1',
+ },
+ }
+ )
+ expect(res.status).toBe(200)
+ const { pageProps: props } = await res.json()
+ expect(props.query).toEqual({ id: '321', hello: 'world' })
+ expect(props.params).toEqual({ id: '321' })
+ })
+
+ if ((global as any).isNextStart) {
+ it('should have data routes in routes-manifest', async () => {
+ const manifest = JSON.parse(
+ await next.readFile('.next/routes-manifest.json')
+ )
+
+ for (const route of manifest.dataRoutes) {
+ route.dataRouteRegex = normalizeRegEx(route.dataRouteRegex)
+ }
+
+ expect(manifest.dataRoutes).toEqual([
+ {
+ dataRouteRegex: normalizeRegEx(
+ `^/_next/data/${escapeStringRegexp(next.buildId)}/index.json$`
+ ),
+ page: '/',
+ },
+ {
+ dataRouteRegex: normalizeRegEx(
+ `^/_next/data/${escapeStringRegexp(next.buildId)}/([^/]+?)\\.json$`
+ ),
+ namedDataRouteRegex: `^/_next/data/${escapeStringRegexp(
+ next.buildId
+ )}/(?[^/]+?)\\.json$`,
+ page: '/[id]',
+ routeKeys: {
+ id: 'id',
+ },
+ },
+ ])
+ })
+ }
+})
diff --git a/test/e2e/disabled-tests/middleware-request-header-overrides/app/.gitignore b/test/e2e/disabled-tests/middleware-request-header-overrides/app/.gitignore
new file mode 100644
index 0000000000..e985853ed8
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-request-header-overrides/app/.gitignore
@@ -0,0 +1 @@
+.vercel
diff --git a/test/e2e/disabled-tests/middleware-request-header-overrides/app/middleware.js b/test/e2e/disabled-tests/middleware-request-header-overrides/app/middleware.js
new file mode 100644
index 0000000000..4421a4f37f
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-request-header-overrides/app/middleware.js
@@ -0,0 +1,30 @@
+import { NextResponse } from 'next/server'
+
+/**
+ * @param {import('next/server').NextRequest} request
+ */
+export async function middleware(request) {
+ const headers = new Headers(request.headers)
+ headers.set('x-from-middleware', 'hello-from-middleware')
+
+ const removeHeaders = request.nextUrl.searchParams.get('remove-headers')
+ if (removeHeaders) {
+ for (const key of removeHeaders.split(',')) {
+ headers.delete(key)
+ }
+ }
+
+ const updateHeader = request.nextUrl.searchParams.get('update-headers')
+ if (updateHeader) {
+ for (const kv of updateHeader.split(',')) {
+ const [key, value] = kv.split('=')
+ headers.set(key, value)
+ }
+ }
+
+ return NextResponse.next({
+ request: {
+ headers,
+ },
+ })
+}
diff --git a/test/e2e/disabled-tests/middleware-request-header-overrides/app/next.config.js b/test/e2e/disabled-tests/middleware-request-header-overrides/app/next.config.js
new file mode 100644
index 0000000000..4ba52ba2c8
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-request-header-overrides/app/next.config.js
@@ -0,0 +1 @@
+module.exports = {}
diff --git a/test/e2e/disabled-tests/middleware-request-header-overrides/app/pages/api/dump-headers-edge.js b/test/e2e/disabled-tests/middleware-request-header-overrides/app/pages/api/dump-headers-edge.js
new file mode 100644
index 0000000000..0ece8ea2c7
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-request-header-overrides/app/pages/api/dump-headers-edge.js
@@ -0,0 +1,11 @@
+export const config = {
+ runtime: 'experimental-edge',
+}
+
+export default (req) => {
+ return Response.json(Object.fromEntries(req.headers.entries()), {
+ headers: {
+ 'headers-from-edge-function': '1',
+ },
+ })
+}
diff --git a/test/e2e/disabled-tests/middleware-request-header-overrides/app/pages/api/dump-headers-serverless.js b/test/e2e/disabled-tests/middleware-request-header-overrides/app/pages/api/dump-headers-serverless.js
new file mode 100644
index 0000000000..0f1a9262d9
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-request-header-overrides/app/pages/api/dump-headers-serverless.js
@@ -0,0 +1,6 @@
+export default (req, res) => {
+ return res
+ .status(200)
+ .setHeader('headers-from-serverless', '1')
+ .json(req.headers)
+}
diff --git a/test/e2e/disabled-tests/middleware-request-header-overrides/app/pages/ssr-page.js b/test/e2e/disabled-tests/middleware-request-header-overrides/app/pages/ssr-page.js
new file mode 100644
index 0000000000..ed2e4a6fcc
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-request-header-overrides/app/pages/ssr-page.js
@@ -0,0 +1,15 @@
+export default function SSRPage({ headers }) {
+ return (
+ <>
+
+ >
+ )
+}
+
+export const getServerSideProps = (ctx) => {
+ return {
+ props: {
+ headers: ctx.req.headers,
+ },
+ }
+}
diff --git a/test/e2e/disabled-tests/middleware-request-header-overrides/test/index.test.ts b/test/e2e/disabled-tests/middleware-request-header-overrides/test/index.test.ts
new file mode 100644
index 0000000000..03f7296b5b
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-request-header-overrides/test/index.test.ts
@@ -0,0 +1,119 @@
+/* eslint-env jest */
+
+import { join } from 'path'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { fetchViaHTTP } from 'next-test-utils'
+import { createNext, FileRef } from 'e2e-utils'
+import cheerio from 'cheerio'
+
+describe('Middleware Request Headers Overrides', () => {
+ let next: NextInstance
+
+ afterAll(() => next.destroy())
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ pages: new FileRef(join(__dirname, '../app/pages')),
+ 'next.config.js': new FileRef(join(__dirname, '../app/next.config.js')),
+ 'middleware.js': new FileRef(join(__dirname, '../app/middleware.js')),
+ },
+ })
+ })
+
+ describe.each([
+ {
+ title: 'Serverless Functions',
+ path: '/api/dump-headers-serverless',
+ toJson: (res: Response) => res.json(),
+ },
+ {
+ title: 'Edge Functions',
+ path: '/api/dump-headers-edge',
+ toJson: (res: Response) => res.json(),
+ },
+ {
+ title: 'getServerSideProps',
+ path: '/ssr-page',
+ toJson: async (res: Response) => {
+ const $ = cheerio.load(await res.text())
+ return JSON.parse($('#headers').text())
+ },
+ },
+ ])('$title Backend', ({ path, toJson }) => {
+ it(`Adds new headers`, async () => {
+ const res = await fetchViaHTTP(next.url, path, null, {
+ headers: {
+ 'x-from-client': 'hello-from-client',
+ },
+ })
+ expect(await toJson(res)).toMatchObject({
+ 'x-from-client': 'hello-from-client',
+ 'x-from-middleware': 'hello-from-middleware',
+ })
+ })
+
+ it(`Deletes headers`, async () => {
+ const res = await fetchViaHTTP(
+ next.url,
+ path,
+ {
+ 'remove-headers': 'x-from-client1,x-from-client2',
+ },
+ {
+ headers: {
+ 'x-from-client1': 'hello-from-client',
+ 'X-From-Client2': 'hello-from-client',
+ },
+ }
+ )
+
+ const json = await toJson(res)
+ expect(json).not.toHaveProperty('x-from-client1')
+ expect(json).not.toHaveProperty('X-From-Client2')
+ expect(json).toMatchObject({
+ 'x-from-middleware': 'hello-from-middleware',
+ })
+
+ // Should not be included in response headers.
+ expect(res.headers.get('x-middleware-override-headers')).toBeNull()
+ expect(
+ res.headers.get('x-middleware-request-x-from-middleware')
+ ).toBeNull()
+ expect(res.headers.get('x-middleware-request-x-from-client1')).toBeNull()
+ expect(res.headers.get('x-middleware-request-x-from-client2')).toBeNull()
+ })
+
+ it(`Updates headers`, async () => {
+ const res = await fetchViaHTTP(
+ next.url,
+ path,
+ {
+ 'update-headers':
+ 'x-from-client1=new-value1,x-from-client2=new-value2',
+ },
+ {
+ headers: {
+ 'x-from-client1': 'old-value1',
+ 'X-From-Client2': 'old-value2',
+ 'x-from-client3': 'old-value3',
+ },
+ }
+ )
+ expect(await toJson(res)).toMatchObject({
+ 'x-from-client1': 'new-value1',
+ 'x-from-client2': 'new-value2',
+ 'x-from-client3': 'old-value3',
+ 'x-from-middleware': 'hello-from-middleware',
+ })
+
+ // Should not be included in response headers.
+ expect(res.headers.get('x-middleware-override-headers')).toBeNull()
+ expect(
+ res.headers.get('x-middleware-request-x-from-middleware')
+ ).toBeNull()
+ expect(res.headers.get('x-middleware-request-x-from-client1')).toBeNull()
+ expect(res.headers.get('x-middleware-request-x-from-client2')).toBeNull()
+ expect(res.headers.get('x-middleware-request-x-from-client3')).toBeNull()
+ })
+ })
+})
diff --git a/test/e2e/disabled-tests/middleware-trailing-slash/app/middleware.js b/test/e2e/disabled-tests/middleware-trailing-slash/app/middleware.js
new file mode 100644
index 0000000000..4d6d5b3204
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-trailing-slash/app/middleware.js
@@ -0,0 +1,106 @@
+import { NextResponse, URLPattern } from 'next/server'
+
+export async function middleware(request) {
+ const url = request.nextUrl
+
+ // this is needed for tests to get the BUILD_ID
+ if (url.pathname.startsWith('/_next/static/__BUILD_ID')) {
+ return NextResponse.next()
+ }
+
+ if (request.headers.get('x-prerender-revalidate')) {
+ return NextResponse.next({
+ headers: { 'x-middleware': 'hi' },
+ })
+ }
+
+ if (url.pathname === '/about/') {
+ return NextResponse.rewrite(new URL('/about/a', request.url))
+ }
+
+ if (url.pathname === '/ssr-page/') {
+ url.pathname = '/ssr-page-2'
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname === '/') {
+ url.pathname = '/ssg/first'
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname === '/to-ssg/') {
+ url.pathname = '/ssg/hello'
+ url.searchParams.set('from', 'middleware')
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname === '/sha/') {
+ url.pathname = '/shallow'
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname === '/rewrite-to-dynamic/') {
+ url.pathname = '/blog/from-middleware'
+ url.searchParams.set('some', 'middleware')
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname === '/rewrite-to-config-rewrite/') {
+ url.pathname = '/rewrite-3'
+ url.searchParams.set('some', 'middleware')
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname === '/redirect-to-somewhere/') {
+ url.pathname = '/somewhere'
+ return NextResponse.redirect(url, {
+ headers: {
+ 'x-redirect-header': 'hi',
+ },
+ })
+ }
+
+ const original = new URL(request.url)
+ return NextResponse.next({
+ headers: {
+ 'req-url-path': `${original.pathname}${original.search}`,
+ 'req-url-basepath': request.nextUrl.basePath,
+ 'req-url-pathname': request.nextUrl.pathname,
+ 'req-url-query': request.nextUrl.searchParams.get('foo'),
+ 'req-url-locale': request.nextUrl.locale,
+ 'req-url-params':
+ url.pathname !== '/static' ? JSON.stringify(params(request.url)) : '{}',
+ },
+ })
+}
+
+const PATTERNS = [
+ [
+ new URLPattern({ pathname: '/:locale/:id' }),
+ ({ pathname }) => ({
+ pathname: '/:locale/:id',
+ params: pathname.groups,
+ }),
+ ],
+ [
+ new URLPattern({ pathname: '/:id' }),
+ ({ pathname }) => ({
+ pathname: '/:id',
+ params: pathname.groups,
+ }),
+ ],
+]
+
+const params = (url) => {
+ const input = url.split('?')[0]
+ let result = {}
+
+ for (const [pattern, handler] of PATTERNS) {
+ const patternResult = pattern.exec(input)
+ if (patternResult !== null && 'pathname' in patternResult) {
+ result = handler(patternResult)
+ break
+ }
+ }
+ return result
+}
diff --git a/test/e2e/disabled-tests/middleware-trailing-slash/app/next.config.js b/test/e2e/disabled-tests/middleware-trailing-slash/app/next.config.js
new file mode 100644
index 0000000000..0c482151c3
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-trailing-slash/app/next.config.js
@@ -0,0 +1,32 @@
+module.exports = {
+ trailingSlash: true,
+ redirects() {
+ return [
+ {
+ source: '/redirect-1',
+ destination: '/somewhere/else/',
+ permanent: false,
+ },
+ ]
+ },
+ rewrites() {
+ return [
+ {
+ source: '/rewrite-1',
+ destination: '/ssr-page?from=config',
+ },
+ {
+ source: '/rewrite-2',
+ destination: '/about/a?from=next-config',
+ },
+ {
+ source: '/sha',
+ destination: '/shallow',
+ },
+ {
+ source: '/rewrite-3',
+ destination: '/blog/middleware-rewrite?hello=config',
+ },
+ ]
+ },
+}
diff --git a/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/[id].js b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/[id].js
new file mode 100644
index 0000000000..11f4614a67
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/[id].js
@@ -0,0 +1,3 @@
+export default function Index() {
+ return Dynamic route
+}
diff --git a/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/_app.js b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/_app.js
new file mode 100644
index 0000000000..65e905445e
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/_app.js
@@ -0,0 +1,8 @@
+export default function App({ Component, pageProps }) {
+ if (!pageProps || typeof pageProps !== 'object') {
+ throw new Error(
+ `Invariant: received invalid pageProps in _app, received ${pageProps}`
+ )
+ }
+ return
+}
diff --git a/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/about/a.js b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/about/a.js
new file mode 100644
index 0000000000..dfacd4eaff
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/about/a.js
@@ -0,0 +1,7 @@
+export default function AboutA() {
+ return (
+
+
AboutA
+
+ )
+}
diff --git a/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/about/b.js b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/about/b.js
new file mode 100644
index 0000000000..7a44254c45
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/about/b.js
@@ -0,0 +1,7 @@
+export default function AboutB() {
+ return (
+
+
AboutB
+
+ )
+}
diff --git a/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/api/headers.js b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/api/headers.js
new file mode 100644
index 0000000000..0f65c82e9f
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/api/headers.js
@@ -0,0 +1,3 @@
+export default function handler(req, res) {
+ res.json({ url: req.url, headers: req.headers })
+}
diff --git a/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/blog/[slug].js b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/blog/[slug].js
new file mode 100644
index 0000000000..590ca0dcc1
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/blog/[slug].js
@@ -0,0 +1,23 @@
+import { useRouter } from 'next/router'
+
+export default function Page(props) {
+ const router = useRouter()
+ return (
+ <>
+ /blog/[slug]
+ {JSON.stringify(router.query)}
+ {router.pathname}
+ {router.asPath}
+ {JSON.stringify(props)}
+ >
+ )
+}
+
+export function getServerSideProps({ params }) {
+ return {
+ props: {
+ now: Date.now(),
+ params,
+ },
+ }
+}
diff --git a/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/error-throw.js b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/error-throw.js
new file mode 100644
index 0000000000..b8a8706e5f
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/error-throw.js
@@ -0,0 +1,12 @@
+export default function ThrowOnData({ message }) {
+ return (
+
+
Throw on data request
+
{message}
+
+ )
+}
+
+export const getServerSideProps = ({ query }) => ({
+ props: { message: query.message || '' },
+})
diff --git a/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/error.js b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/error.js
new file mode 100644
index 0000000000..a5a49734cc
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/error.js
@@ -0,0 +1,11 @@
+import Link from 'next/link'
+
+export default function Errors() {
+ return (
+
+
+ Throw on data
+
+
+ )
+}
diff --git a/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/shallow.js b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/shallow.js
new file mode 100644
index 0000000000..4d1c8564ee
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/shallow.js
@@ -0,0 +1,43 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+export default function Shallow({ message }) {
+ const { pathname, query } = useRouter()
+ return (
+
+
+ {message}
+
+
+ Shallow link to ?hello=world
+
+
+
+
+ Deep link to ?hello=goodbye
+
+
+
+
+ Current path: {pathname}
+
+
+
+
+ Current query: {JSON.stringify(query)}
+
+
+
+
+ )
+}
+
+let i = 0
+
+export const getServerSideProps = () => {
+ return {
+ props: {
+ message: `Random: ${++i}${Math.random()}`,
+ },
+ }
+}
diff --git a/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/ssg/[slug].js b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/ssg/[slug].js
new file mode 100644
index 0000000000..cf9fb1db0b
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/ssg/[slug].js
@@ -0,0 +1,42 @@
+import { useRouter } from 'next/router'
+import { useEffect } from 'react'
+import { useState } from 'react'
+
+export default function Page(props) {
+ const router = useRouter()
+ const [asPath, setAsPath] = useState(
+ router.isReady ? router.asPath : router.href
+ )
+
+ useEffect(() => {
+ if (router.isReady) {
+ setAsPath(router.asPath)
+ }
+ }, [router.asPath, router.isReady])
+
+ return (
+ <>
+ /blog/[slug]
+ {JSON.stringify(router.query)}
+ {router.pathname}
+ {asPath}
+ {JSON.stringify(props)}
+ >
+ )
+}
+
+export function getStaticProps({ params }) {
+ return {
+ props: {
+ now: Date.now(),
+ params,
+ },
+ }
+}
+
+export function getStaticPaths() {
+ return {
+ paths: ['/ssg/first', '/ssg/hello'],
+ fallback: 'blocking',
+ }
+}
diff --git a/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/ssr-page-2.js b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/ssr-page-2.js
new file mode 100644
index 0000000000..67e1b70868
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/ssr-page-2.js
@@ -0,0 +1,11 @@
+export default function SSRPage(props) {
+ return {props.message}
+}
+
+export const getServerSideProps = (req) => {
+ return {
+ props: {
+ message: 'Bye Cruel World',
+ },
+ }
+}
diff --git a/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/ssr-page.js b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/ssr-page.js
new file mode 100644
index 0000000000..e2aaaa56b4
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-trailing-slash/app/pages/ssr-page.js
@@ -0,0 +1,11 @@
+export default function SSRPage(props) {
+ return {props.message}
+}
+
+export const getServerSideProps = (req) => {
+ return {
+ props: {
+ message: 'Hello World',
+ },
+ }
+}
diff --git a/test/e2e/disabled-tests/middleware-trailing-slash/test/index.test.ts b/test/e2e/disabled-tests/middleware-trailing-slash/test/index.test.ts
new file mode 100644
index 0000000000..58f545940a
--- /dev/null
+++ b/test/e2e/disabled-tests/middleware-trailing-slash/test/index.test.ts
@@ -0,0 +1,412 @@
+/* eslint-env jest */
+
+import fs from 'fs-extra'
+import { join } from 'path'
+import webdriver from 'next-webdriver'
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { check, fetchViaHTTP, waitFor } from 'next-test-utils'
+
+describe('Middleware Runtime trailing slash', () => {
+ let next: NextInstance
+
+ afterAll(async () => {
+ await next.destroy()
+ })
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ 'next.config.js': new FileRef(join(__dirname, '../app/next.config.js')),
+ 'middleware.js': new FileRef(join(__dirname, '../app/middleware.js')),
+ pages: new FileRef(join(__dirname, '../app/pages')),
+ },
+ })
+ })
+
+ function runTests() {
+ if ((global as any).isNextDev) {
+ it('refreshes the page when middleware changes ', async () => {
+ const browser = await webdriver(next.url, `/about/`)
+ await browser.eval('window.didrefresh = "hello"')
+ const text = await browser.elementByCss('h1').text()
+ expect(text).toEqual('AboutA')
+
+ const middlewarePath = join(next.testDir, '/middleware.js')
+ const originalContent = fs.readFileSync(middlewarePath, 'utf-8')
+ const editedContent = originalContent.replace('/about/a', '/about/b')
+
+ try {
+ fs.writeFileSync(middlewarePath, editedContent)
+ await waitFor(1000)
+ const textb = await browser.elementByCss('h1').text()
+ expect(await browser.eval('window.itdidnotrefresh')).not.toBe('hello')
+ expect(textb).toEqual('AboutB')
+ } finally {
+ fs.writeFileSync(middlewarePath, originalContent)
+ await browser.close()
+ }
+ })
+ }
+
+ if ((global as any).isNextStart) {
+ it('should have valid middleware field in manifest', async () => {
+ const manifest = await fs.readJSON(
+ join(next.testDir, '.next/server/middleware-manifest.json')
+ )
+ expect(manifest.middleware).toEqual({
+ '/': {
+ files: ['server/edge-runtime-webpack.js', 'server/middleware.js'],
+ name: 'middleware',
+ env: [],
+ page: '/',
+ matchers: [{ regexp: '^/.*$' }],
+ wasm: [],
+ assets: [],
+ },
+ })
+ })
+
+ it('should have correct files in manifest', async () => {
+ const manifest = await fs.readJSON(
+ join(next.testDir, '.next/server/middleware-manifest.json')
+ )
+ for (const key of Object.keys(manifest.middleware)) {
+ const middleware = manifest.middleware[key]
+ expect(middleware.files).toContainEqual(
+ expect.stringContaining('server/edge-runtime-webpack')
+ )
+ expect(middleware.files).not.toContainEqual(
+ expect.stringContaining('static/chunks/')
+ )
+ }
+ })
+
+ it('should not run middleware for on-demand revalidate', async () => {
+ const bypassToken = (
+ await fs.readJSON(join(next.testDir, '.next/prerender-manifest.json'))
+ ).preview.previewModeId
+
+ const res = await fetchViaHTTP(next.url, '/ssg/first/', undefined, {
+ headers: {
+ 'x-prerender-revalidate': bypassToken,
+ },
+ })
+ expect(res.status).toBe(200)
+ expect(res.headers.get('x-middleware')).toBeFalsy()
+ expect(res.headers.get('x-nextjs-cache')).toBe('REVALIDATED')
+ })
+ }
+
+ it('should have init header for NextResponse.redirect', async () => {
+ const res = await fetchViaHTTP(
+ next.url,
+ '/redirect-to-somewhere/',
+ undefined,
+ {
+ redirect: 'manual',
+ }
+ )
+ expect(res.status).toBe(307)
+ expect(new URL(res.headers.get('location'), 'http://n').pathname).toBe(
+ '/somewhere/'
+ )
+ expect(res.headers.get('x-redirect-header')).toBe('hi')
+ })
+
+ it('should have correct query values for rewrite to ssg page', async () => {
+ const browser = await webdriver(next.url, '/to-ssg/')
+ await browser.eval('window.beforeNav = 1')
+
+ await check(() => browser.elementByCss('body').text(), /\/to-ssg/)
+
+ expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
+ slug: 'hello',
+ })
+ expect(
+ JSON.parse(await browser.elementByCss('#props').text()).params
+ ).toEqual({
+ slug: 'hello',
+ })
+ expect(await browser.elementByCss('#pathname').text()).toBe('/ssg/[slug]')
+ expect(await browser.elementByCss('#as-path').text()).toBe('/to-ssg/')
+ })
+
+ it('should have correct dynamic route params on client-transition to dynamic route', async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.eval('window.beforeNav = 1')
+ await browser.eval('window.next.router.push("/blog/first")')
+ await browser.waitForElementByCss('#blog')
+
+ expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
+ slug: 'first',
+ })
+ expect(
+ JSON.parse(await browser.elementByCss('#props').text()).params
+ ).toEqual({
+ slug: 'first',
+ })
+ expect(await browser.elementByCss('#pathname').text()).toBe(
+ '/blog/[slug]'
+ )
+ expect(await browser.elementByCss('#as-path').text()).toBe('/blog/first/')
+
+ await browser.eval('window.next.router.push("/blog/second")')
+ await check(() => browser.elementByCss('body').text(), /"slug":"second"/)
+
+ expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
+ slug: 'second',
+ })
+ expect(
+ JSON.parse(await browser.elementByCss('#props').text()).params
+ ).toEqual({
+ slug: 'second',
+ })
+ expect(await browser.elementByCss('#pathname').text()).toBe(
+ '/blog/[slug]'
+ )
+ expect(await browser.elementByCss('#as-path').text()).toBe(
+ '/blog/second/'
+ )
+ })
+
+ it('should have correct dynamic route params for middleware rewrite to dynamic route', async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.eval('window.beforeNav = 1')
+ await browser.eval('window.next.router.push("/rewrite-to-dynamic")')
+ await browser.waitForElementByCss('#blog')
+
+ expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
+ slug: 'from-middleware',
+ some: 'middleware',
+ })
+ expect(
+ JSON.parse(await browser.elementByCss('#props').text()).params
+ ).toEqual({
+ slug: 'from-middleware',
+ })
+ expect(await browser.elementByCss('#pathname').text()).toBe(
+ '/blog/[slug]'
+ )
+ expect(await browser.elementByCss('#as-path').text()).toBe(
+ '/rewrite-to-dynamic/'
+ )
+ })
+
+ it('should have correct route params for chained rewrite from middleware to config rewrite', async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.eval('window.beforeNav = 1')
+ await browser.eval(
+ 'window.next.router.push("/rewrite-to-config-rewrite")'
+ )
+ await browser.waitForElementByCss('#blog')
+
+ expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
+ slug: 'middleware-rewrite',
+ hello: 'config',
+ some: 'middleware',
+ })
+ expect(
+ JSON.parse(await browser.elementByCss('#props').text()).params
+ ).toEqual({
+ slug: 'middleware-rewrite',
+ })
+ expect(await browser.elementByCss('#pathname').text()).toBe(
+ '/blog/[slug]'
+ )
+ expect(await browser.elementByCss('#as-path').text()).toBe(
+ '/rewrite-to-config-rewrite/'
+ )
+ })
+
+ it('should have correct route params for rewrite from config dynamic route', async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.eval('window.beforeNav = 1')
+ await browser.eval('window.next.router.push("/rewrite-3")')
+ await browser.waitForElementByCss('#blog')
+
+ expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
+ slug: 'middleware-rewrite',
+ hello: 'config',
+ })
+ expect(
+ JSON.parse(await browser.elementByCss('#props').text()).params
+ ).toEqual({
+ slug: 'middleware-rewrite',
+ })
+ expect(await browser.elementByCss('#pathname').text()).toBe(
+ '/blog/[slug]'
+ )
+ expect(await browser.elementByCss('#as-path').text()).toBe('/rewrite-3/')
+ })
+
+ it('should have correct route params for rewrite from config non-dynamic route', async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.eval('window.beforeNav = 1')
+ await browser.eval('window.next.router.push("/rewrite-1")')
+
+ await check(
+ () => browser.eval('document.documentElement.innerHTML'),
+ /Hello World/
+ )
+
+ expect(await browser.eval('window.next.router.query')).toEqual({
+ from: 'config',
+ })
+ })
+
+ it('should redirect the same for direct visit and client-transition', async () => {
+ const res = await fetchViaHTTP(next.url, `/redirect-1/`, undefined, {
+ redirect: 'manual',
+ })
+ expect(res.status).toBe(307)
+ expect(new URL(res.headers.get('location'), 'http://n').pathname).toBe(
+ '/somewhere/else/'
+ )
+
+ const browser = await webdriver(next.url, `/`)
+ await browser.eval(`next.router.push('/redirect-1')`)
+ await check(async () => {
+ const pathname = await browser.eval('location.pathname')
+ return pathname === '/somewhere/else/' ? 'success' : pathname
+ }, 'success')
+ })
+
+ it('should rewrite the same for direct visit and client-transition', async () => {
+ const res = await fetchViaHTTP(next.url, `/rewrite-1/`)
+ expect(res.status).toBe(200)
+ expect(await res.text()).toContain('Hello World')
+
+ const browser = await webdriver(next.url, `/`)
+ await browser.eval('window.beforeNav = 1')
+ await browser.eval(`next.router.push('/rewrite-1')`)
+ await check(async () => {
+ const content = await browser.eval('document.documentElement.innerHTML')
+ return content.includes('Hello World') ? 'success' : content
+ }, 'success')
+ expect(await browser.eval('window.beforeNav')).toBe(1)
+ })
+
+ it('should rewrite correctly for non-SSG/SSP page', async () => {
+ const res = await fetchViaHTTP(next.url, `/rewrite-2/`)
+ expect(res.status).toBe(200)
+ expect(await res.text()).toContain('AboutA')
+
+ const browser = await webdriver(next.url, `/`)
+ await browser.eval(`next.router.push('/rewrite-2')`)
+ await check(async () => {
+ const content = await browser.eval('document.documentElement.innerHTML')
+ return content.includes('AboutA') ? 'success' : content
+ }, 'success')
+ })
+
+ it('should respond with 400 on decode failure', async () => {
+ const res = await fetchViaHTTP(next.url, `/%2/`)
+ expect(res.status).toBe(400)
+
+ if ((global as any).isNextStart) {
+ expect(await res.text()).toContain('Bad Request')
+ }
+ })
+
+ it(`should validate & parse request url from any route`, async () => {
+ const res = await fetchViaHTTP(next.url, `/static/`)
+
+ expect(res.headers.get('req-url-basepath')).toBeFalsy()
+ expect(res.headers.get('req-url-pathname')).toBe('/static/')
+
+ const { pathname, params } = JSON.parse(res.headers.get('req-url-params'))
+ expect(pathname).toBe(undefined)
+ expect(params).toEqual(undefined)
+
+ expect(res.headers.get('req-url-query')).not.toBe('bar')
+ })
+
+ it('should trigger middleware for data requests', async () => {
+ const browser = await webdriver(next.url, `/ssr-page`)
+ const text = await browser.elementByCss('h1').text()
+ expect(text).toEqual('Bye Cruel World')
+ const res = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/ssr-page.json`,
+ undefined,
+ {
+ headers: {
+ 'x-nextjs-data': '1',
+ },
+ }
+ )
+ const json = await res.json()
+ expect(json.pageProps.message).toEqual('Bye Cruel World')
+ })
+
+ it('should normalize data requests into page requests', async () => {
+ const res = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/send-url.json`,
+ undefined,
+ {
+ headers: {
+ 'x-nextjs-data': '1',
+ },
+ }
+ )
+ expect(res.headers.get('req-url-path')).toEqual('/send-url/')
+ })
+
+ it('should keep non data requests in their original shape', async () => {
+ const res = await fetchViaHTTP(
+ next.url,
+ `/_next/static/${next.buildId}/_devMiddlewareManifest.json?foo=1`
+ )
+ expect(res.headers.get('req-url-path')).toEqual(
+ `/_next/static/${next.buildId}/_devMiddlewareManifest.json?foo=1`
+ )
+ })
+
+ it('should add a rewrite header on data requests for rewrites', async () => {
+ const res = await fetchViaHTTP(next.url, `/ssr-page/`)
+ const dataRes = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/ssr-page.json`,
+ undefined,
+ { headers: { 'x-nextjs-data': '1' } }
+ )
+ const json = await dataRes.json()
+ expect(json.pageProps.message).toEqual('Bye Cruel World')
+ expect(res.headers.get('x-nextjs-matched-path')).toBeNull()
+ expect(dataRes.headers.get('x-nextjs-matched-path')).toEqual(
+ `/ssr-page-2`
+ )
+ })
+
+ it('allows shallow linking with middleware', async () => {
+ const browser = await webdriver(next.url, '/sha/')
+ const getMessageContents = () =>
+ browser.elementById('message-contents').text()
+ const ssrMessage = await getMessageContents()
+ const requests: string[] = []
+
+ browser.on('request', (x) => {
+ requests.push(x.url())
+ })
+
+ browser.elementById('deep-link').click()
+ browser.waitForElementByCss('[data-query-hello="goodbye"]')
+ const deepLinkMessage = await getMessageContents()
+ expect(deepLinkMessage).not.toEqual(ssrMessage)
+
+ // Changing the route with a shallow link should not cause a server request
+ browser.elementById('shallow-link').click()
+ browser.waitForElementByCss('[data-query-hello="world"]')
+ expect(await getMessageContents()).toEqual(deepLinkMessage)
+
+ // Check that no server requests were made to ?hello=world,
+ // as it's a shallow request.
+ expect(requests.filter((req) => req.includes('_next/data'))).toEqual([
+ `${next.url}/_next/data/${next.buildId}/sha.json?hello=goodbye`,
+ ])
+ })
+ }
+
+ runTests()
+})
diff --git a/test/e2e/disabled-tests/proxy-request-with-middleware/app/middleware.js b/test/e2e/disabled-tests/proxy-request-with-middleware/app/middleware.js
new file mode 100644
index 0000000000..e045282b9f
--- /dev/null
+++ b/test/e2e/disabled-tests/proxy-request-with-middleware/app/middleware.js
@@ -0,0 +1,5 @@
+import { NextResponse } from 'next/server'
+
+export async function middleware() {
+ return NextResponse.next()
+}
diff --git a/test/e2e/disabled-tests/proxy-request-with-middleware/app/pages/api/index.js b/test/e2e/disabled-tests/proxy-request-with-middleware/app/pages/api/index.js
new file mode 100644
index 0000000000..01c9f6d808
--- /dev/null
+++ b/test/e2e/disabled-tests/proxy-request-with-middleware/app/pages/api/index.js
@@ -0,0 +1,28 @@
+import request from 'request'
+
+export const config = {
+ api: {
+ bodyParser: false,
+ },
+}
+
+export default function handler(req, res) {
+ return req
+ .pipe(
+ request(
+ `http://${
+ // node v18 resolves to IPv6 by default so force IPv4
+ process.version.startsWith('v18.')
+ ? `127.0.0.1:${req.headers.host.split(':').pop() || ''}`
+ : req.headers.host
+ }${req.url}/post`,
+ {
+ followAllRedirects: false,
+ followRedirect: false,
+ gzip: true,
+ json: false,
+ }
+ )
+ )
+ .pipe(res)
+}
diff --git a/test/e2e/disabled-tests/proxy-request-with-middleware/app/pages/api/post.js b/test/e2e/disabled-tests/proxy-request-with-middleware/app/pages/api/post.js
new file mode 100644
index 0000000000..172130f449
--- /dev/null
+++ b/test/e2e/disabled-tests/proxy-request-with-middleware/app/pages/api/post.js
@@ -0,0 +1,12 @@
+export const config = {
+ api: {
+ bodyParser: false,
+ },
+}
+
+export default function handler(req, res) {
+ return res.json({
+ method: req.method,
+ headers: req.headers,
+ })
+}
diff --git a/test/e2e/disabled-tests/proxy-request-with-middleware/test/index.test.ts b/test/e2e/disabled-tests/proxy-request-with-middleware/test/index.test.ts
new file mode 100644
index 0000000000..3795ca3052
--- /dev/null
+++ b/test/e2e/disabled-tests/proxy-request-with-middleware/test/index.test.ts
@@ -0,0 +1,53 @@
+/* eslint-env jest */
+
+import { join } from 'path'
+import { fetchViaHTTP } from 'next-test-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { createNext, FileRef } from 'e2e-utils'
+
+describe('Requests not effected when middleware used', () => {
+ let next: NextInstance
+
+ afterAll(() => next.destroy())
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ pages: new FileRef(join(__dirname, '../app/pages')),
+ 'middleware.js': new FileRef(join(__dirname, '../app/middleware.js')),
+ },
+ dependencies: {
+ request: '^2.88.2',
+ },
+ })
+ })
+
+ function sendRequest(method) {
+ const body = !['get', 'head'].includes(method.toLowerCase())
+ ? JSON.stringify({
+ key: 'value',
+ })
+ : undefined
+ it(`should proxy ${method} request ${
+ body ? 'with body' : ''
+ }`, async () => {
+ const headers = {
+ 'content-type': 'application/json',
+ 'x-custom-header': 'some value',
+ }
+ const res = await fetchViaHTTP(next.url, `api`, '', {
+ method: method.toUpperCase(),
+ headers,
+ body: method.toLowerCase() !== 'get' ? body : undefined,
+ })
+ const data = await res.json()
+ expect(data.method).toEqual(method)
+ if (body) {
+ expect(data.headers['content-length']).toEqual(String(body.length))
+ }
+ expect(data.headers).toEqual(expect.objectContaining(headers))
+ })
+ }
+
+ sendRequest('GET')
+ sendRequest('POST')
+})
diff --git a/test/e2e/jest.config.all.js b/test/e2e/jest.config.all.js
new file mode 100644
index 0000000000..5e688d82a8
--- /dev/null
+++ b/test/e2e/jest.config.all.js
@@ -0,0 +1,11 @@
+// @ts-check
+/** @type {import('@jest/types').Config.InitialOptions} */
+
+const parent = require('./jest.config')
+
+const config = {
+ ...parent,
+ testMatch: ['**/test/e2e/**/*.test.js', '**/test/e2e/**/*.test.ts'],
+}
+
+module.exports = config
diff --git a/test/e2e/jest.config.disabled.js b/test/e2e/jest.config.disabled.js
new file mode 100644
index 0000000000..59eb69a076
--- /dev/null
+++ b/test/e2e/jest.config.disabled.js
@@ -0,0 +1,11 @@
+// @ts-check
+/** @type {import('@jest/types').Config.InitialOptions} */
+
+const parent = require('./jest.config')
+
+const config = {
+ ...parent,
+ testMatch: ['**/test/e2e/disabled-tests/**/*.test.js', '**/test/e2e/disabled-tests/**/*.test.ts'],
+}
+
+module.exports = config
diff --git a/test/e2e/jest.config.js b/test/e2e/jest.config.js
index b9fcbe1b58..c1960d0c0a 100644
--- a/test/e2e/jest.config.js
+++ b/test/e2e/jest.config.js
@@ -2,14 +2,20 @@
/** @type {import('@jest/types').Config.InitialOptions} */
const config = {
+ maxWorkers: 1,
rootDir: __dirname,
- setupFiles: ['../../jestSetup.js'],
- testMatch: ['**/test/**/*.test.js', '**/test/**/*.test.ts'],
+ testMatch: [
+ '**/test/e2e/tests/**/*.test.js',
+ '**/test/e2e/tests/**/*.test.ts',
+ '**/test/e2e/modified-tests/**/*.test.js',
+ '**/test/e2e/modified-tests/**/*.test.ts',
+ ],
transform: {
'\\.[jt]sx?$': 'babel-jest',
},
verbose: true,
testTimeout: 60000,
+ moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],
moduleNameMapper: {
'e2e-utils': '/next-test-lib/e2e-utils.ts',
'test/lib/next-modes/base': '/next-test-lib/next-modes/base.ts',
diff --git a/test/e2e/modified-tests/edge-can-read-request-body/app/.gitignore b/test/e2e/modified-tests/edge-can-read-request-body/app/.gitignore
new file mode 100644
index 0000000000..e985853ed8
--- /dev/null
+++ b/test/e2e/modified-tests/edge-can-read-request-body/app/.gitignore
@@ -0,0 +1 @@
+.vercel
diff --git a/test/e2e/modified-tests/edge-can-read-request-body/app/middleware.js b/test/e2e/modified-tests/edge-can-read-request-body/app/middleware.js
new file mode 100644
index 0000000000..bbddddb07e
--- /dev/null
+++ b/test/e2e/modified-tests/edge-can-read-request-body/app/middleware.js
@@ -0,0 +1,49 @@
+// @ts-check
+
+import { NextResponse } from 'next/server'
+
+/**
+ * @param {NextRequest} req
+ */
+export default async function middleware(req) {
+ const res = NextResponse.next()
+ res.headers.set('x-incoming-content-type', req.headers.get('content-type'))
+
+ const handler =
+ bodyHandlers[req.nextUrl.searchParams.get('middleware-handler')]
+ const headers = await handler?.(req)
+ for (const [key, value] of headers ?? []) {
+ res.headers.set(key, value)
+ }
+
+ return res
+}
+
+/**
+ * @typedef {import('next/server').NextRequest} NextRequest
+ * @typedef {(req: NextRequest) => Promise<[string, string][]>} Handler
+ * @type {Record}
+ */
+const bodyHandlers = {
+ json: async (req) => {
+ const json = await req.json()
+ return [
+ ['x-req-type', 'json'],
+ ['x-serialized', JSON.stringify(json)],
+ ]
+ },
+ text: async (req) => {
+ const text = await req.text()
+ return [
+ ['x-req-type', 'text'],
+ ['x-serialized', text],
+ ]
+ },
+ formData: async (req) => {
+ const formData = await req.formData()
+ return [
+ ['x-req-type', 'formData'],
+ ['x-serialized', JSON.stringify(Object.fromEntries(formData))],
+ ]
+ },
+}
diff --git a/test/e2e/modified-tests/edge-can-read-request-body/app/pages/api/nothing.js b/test/e2e/modified-tests/edge-can-read-request-body/app/pages/api/nothing.js
new file mode 100644
index 0000000000..7c595baeca
--- /dev/null
+++ b/test/e2e/modified-tests/edge-can-read-request-body/app/pages/api/nothing.js
@@ -0,0 +1,3 @@
+export default (_req, res) => {
+ res.send('ok')
+}
diff --git a/test/e2e/modified-tests/edge-can-read-request-body/index.test.ts b/test/e2e/modified-tests/edge-can-read-request-body/index.test.ts
new file mode 100644
index 0000000000..5c4bde573a
--- /dev/null
+++ b/test/e2e/modified-tests/edge-can-read-request-body/index.test.ts
@@ -0,0 +1,104 @@
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils'
+import FormData from 'form-data'
+import path from 'path'
+
+async function serialize(response: Response) {
+ return {
+ text: await response.text(),
+ headers: Object.fromEntries(response.headers),
+ status: response.status,
+ }
+}
+
+describe('Edge can read request body', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: new FileRef(path.resolve(__dirname, './app')),
+ dependencies: {},
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('renders the static page', async () => {
+ const html = await renderViaHTTP(next.url, '/api/nothing')
+ expect(html).toContain('ok')
+ })
+ // NTL Fail
+ ;(process.env.RUN_SKIPPED_TESTS ? describe : describe.skip)('middleware', () => {
+ it('reads a JSON body', async () => {
+ const response = await fetchViaHTTP(next.url, '/api/nothing?middleware-handler=json', null, {
+ method: 'POST',
+ body: JSON.stringify({ hello: 'world' }),
+ })
+ expect(await serialize(response)).toMatchObject({
+ text: expect.stringContaining('ok'),
+ status: 200,
+ headers: {
+ 'x-req-type': 'json',
+ 'x-serialized': '{"hello":"world"}',
+ },
+ })
+ })
+
+ it('reads a text body', async () => {
+ try {
+ const response = await fetchViaHTTP(next.url, '/api/nothing?middleware-handler=text', null, {
+ method: 'POST',
+ body: JSON.stringify({ hello: 'world' }),
+ })
+
+ expect(await serialize(response)).toMatchObject({
+ text: expect.stringContaining('ok'),
+ status: 200,
+ headers: {
+ 'x-req-type': 'text',
+ 'x-serialized': '{"hello":"world"}',
+ },
+ })
+ } catch (err) {
+ console.log('FAILED', err)
+ }
+ })
+
+ it('reads an URL encoded form data', async () => {
+ const response = await fetchViaHTTP(next.url, '/api/nothing?middleware-handler=formData', null, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: new URLSearchParams({ hello: 'world' }).toString(),
+ })
+ expect(await serialize(response)).toMatchObject({
+ text: expect.stringContaining('ok'),
+ status: 200,
+ headers: {
+ 'x-req-type': 'formData',
+ 'x-serialized': '{"hello":"world"}',
+ },
+ })
+ })
+
+ it('reads a multipart form data', async () => {
+ const formData = new FormData()
+ formData.append('hello', 'world')
+
+ const response = await fetchViaHTTP(next.url, '/api/nothing?middleware-handler=formData', null, {
+ method: 'POST',
+ body: formData,
+ })
+
+ expect(await serialize(response)).toMatchObject({
+ text: expect.stringContaining('ok'),
+ status: 200,
+ headers: {
+ 'x-req-type': 'formData',
+ 'x-serialized': '{"hello":"world"}',
+ },
+ })
+ })
+ })
+})
diff --git a/test/e2e/modified-tests/edge-can-use-wasm-files/add.wasm b/test/e2e/modified-tests/edge-can-use-wasm-files/add.wasm
new file mode 100644
index 0000000000..f22496d0b6
Binary files /dev/null and b/test/e2e/modified-tests/edge-can-use-wasm-files/add.wasm differ
diff --git a/test/e2e/modified-tests/edge-can-use-wasm-files/index.test.ts b/test/e2e/modified-tests/edge-can-use-wasm-files/index.test.ts
new file mode 100644
index 0000000000..aa87a1ebca
--- /dev/null
+++ b/test/e2e/modified-tests/edge-can-use-wasm-files/index.test.ts
@@ -0,0 +1,146 @@
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { fetchViaHTTP } from 'next-test-utils'
+import path from 'path'
+import fs from 'fs-extra'
+
+function extractJSON(response) {
+ return JSON.parse(response.headers.get('data') ?? '{}')
+}
+
+function baseNextConfig(): Parameters[0] {
+ return {
+ files: {
+ 'src/add.wasm': new FileRef(path.join(__dirname, './add.wasm')),
+ 'src/add.js': `
+ import wasm from './add.wasm?module'
+ const instance$ = WebAssembly.instantiate(wasm);
+
+ export async function increment(a) {
+ const { instance } = await instance$;
+ return instance.exports.add_one(a);
+ }
+ `,
+ 'pages/index.js': `
+ export default function () { return Hello, world!
}
+ `,
+ 'middleware.js': `
+ import { increment } from './src/add.js'
+ export default async function middleware(request) {
+ const input = Number(request.nextUrl.searchParams.get('input')) || 1;
+ const value = await increment(input);
+ return new Response(null, { headers: { data: JSON.stringify({ input, value }) } });
+ }
+ `,
+ },
+ }
+}
+
+describe('edge api endpoints can use wasm files', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ 'pages/api/add.js': `
+ import { increment } from '../../src/add.js'
+ export default async (request) => {
+ const input = Number(request.nextUrl.searchParams.get('input')) || 1;
+ const value = await increment(input);
+ return new Response(null, { headers: { data: JSON.stringify({ input, value }) } });
+ }
+ export const config = { runtime: 'experimental-edge' };
+ `,
+ 'src/add.wasm': new FileRef(path.join(__dirname, './add.wasm')),
+ 'src/add.js': `
+ import wasm from './add.wasm?module'
+ const instance$ = WebAssembly.instantiate(wasm);
+
+ export async function increment(a) {
+ const { instance } = await instance$;
+ return instance.exports.add_one(a);
+ }
+ `,
+ },
+ })
+ })
+ afterAll(() => next.destroy())
+ it('uses the wasm file', async () => {
+ const response = await fetchViaHTTP(next.url, '/api/add', { input: 10 })
+ expect(extractJSON(response)).toEqual({
+ input: 10,
+ value: 11,
+ })
+ })
+})
+
+describe('middleware can use wasm files', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ const config = baseNextConfig()
+ next = await createNext(config)
+ })
+ afterAll(() => next.destroy())
+
+ it('uses the wasm file', async () => {
+ const response = await fetchViaHTTP(next.url, '/')
+ expect(extractJSON(response)).toEqual({
+ input: 1,
+ value: 2,
+ })
+ })
+
+ it('can be called twice', async () => {
+ const response = await fetchViaHTTP(next.url, '/', { input: 2 })
+ expect(extractJSON(response)).toEqual({
+ input: 2,
+ value: 3,
+ })
+ })
+
+ if (!(global as any).isNextDeploy) {
+ it('lists the necessary wasm bindings in the manifest', async () => {
+ const manifestPath = path.join(next.testDir, '.next/server/middleware-manifest.json')
+ const manifest = await fs.readJSON(manifestPath)
+ expect(manifest.middleware['/']).toMatchObject({
+ wasm: [
+ {
+ filePath: 'server/edge-chunks/wasm_58ccff8b2b94b5dac6ef8957082ecd8f6d34186d.wasm',
+ name: 'wasm_58ccff8b2b94b5dac6ef8957082ecd8f6d34186d',
+ },
+ ],
+ })
+ })
+ }
+})
+
+describe('middleware can use wasm files with the experimental modes on', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ const config = baseNextConfig()
+ config.files['next.config.js'] = `
+ module.exports = {
+ webpack(config) {
+ config.output.webassemblyModuleFilename = 'static/wasm/[modulehash].wasm'
+
+ // Since Webpack 5 doesn't enable WebAssembly by default, we should do it manually
+ config.experiments = { ...config.experiments, asyncWebAssembly: true }
+
+ return config
+ },
+ }
+ `
+ next = await createNext(config)
+ })
+ afterAll(() => next.destroy())
+
+ it('uses the wasm file', async () => {
+ const response = await fetchViaHTTP(next.url, '/')
+ expect(extractJSON(response)).toEqual({
+ input: 1,
+ value: 2,
+ })
+ })
+})
diff --git a/test/e2e/modified-tests/edge-compiler-can-import-blob-assets/app/node_modules/my-pkg/hello/world.json b/test/e2e/modified-tests/edge-compiler-can-import-blob-assets/app/node_modules/my-pkg/hello/world.json
new file mode 100644
index 0000000000..394d6c8bc1
--- /dev/null
+++ b/test/e2e/modified-tests/edge-compiler-can-import-blob-assets/app/node_modules/my-pkg/hello/world.json
@@ -0,0 +1 @@
+{ "i am": "a node dependency" }
diff --git a/test/e2e/modified-tests/edge-compiler-can-import-blob-assets/app/pages/api/edge.js b/test/e2e/modified-tests/edge-compiler-can-import-blob-assets/app/pages/api/edge.js
new file mode 100644
index 0000000000..32264784d7
--- /dev/null
+++ b/test/e2e/modified-tests/edge-compiler-can-import-blob-assets/app/pages/api/edge.js
@@ -0,0 +1,53 @@
+export const config = { runtime: 'experimental-edge' }
+
+/**
+ * @param {import('next/server').NextRequest} req
+ */
+export default async (req) => {
+ const handlerName = req.nextUrl.searchParams.get('handler')
+ const handler = handlers.get(handlerName) || defaultHandler
+ return handler()
+}
+
+/**
+ * @type {Map Promise>}
+ */
+const handlers = new Map([
+ [
+ 'text-file',
+ async () => {
+ const url = new URL('../../src/text-file.txt', import.meta.url)
+ return fetch(url)
+ },
+ ],
+ [
+ 'image-file',
+ async () => {
+ const url = new URL('../../src/vercel.png', import.meta.url)
+ return fetch(url)
+ },
+ ],
+ // [
+ // 'from-node-module',
+ // async () => {
+ // const url = new URL('my-pkg/hello/world.json', import.meta.url)
+ // return fetch(url)
+ // },
+ // ],
+ [
+ 'remote-full',
+ async () => {
+ const url = new URL('https://example.vercel.sh')
+ return fetch(url)
+ },
+ ],
+ [
+ 'remote-with-base',
+ async () => {
+ const url = new URL('/', 'https://example.vercel.sh')
+ return fetch(url)
+ },
+ ],
+])
+
+const defaultHandler = async () => new Response('Invalid handler', { status: 400 })
diff --git a/test/e2e/modified-tests/edge-compiler-can-import-blob-assets/app/src/text-file.txt b/test/e2e/modified-tests/edge-compiler-can-import-blob-assets/app/src/text-file.txt
new file mode 100644
index 0000000000..37607fa1b1
--- /dev/null
+++ b/test/e2e/modified-tests/edge-compiler-can-import-blob-assets/app/src/text-file.txt
@@ -0,0 +1 @@
+Hello, from text-file.txt!
diff --git a/test/e2e/modified-tests/edge-compiler-can-import-blob-assets/app/src/vercel.png b/test/e2e/modified-tests/edge-compiler-can-import-blob-assets/app/src/vercel.png
new file mode 100644
index 0000000000..cb137a989e
Binary files /dev/null and b/test/e2e/modified-tests/edge-compiler-can-import-blob-assets/app/src/vercel.png differ
diff --git a/test/e2e/modified-tests/edge-compiler-can-import-blob-assets/index.test.ts b/test/e2e/modified-tests/edge-compiler-can-import-blob-assets/index.test.ts
new file mode 100644
index 0000000000..bd59e1688c
--- /dev/null
+++ b/test/e2e/modified-tests/edge-compiler-can-import-blob-assets/index.test.ts
@@ -0,0 +1,91 @@
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils'
+import path from 'path'
+import { promises as fs } from 'fs'
+import { readJson } from 'fs-extra'
+import type { MiddlewareManifest } from 'next/build/webpack/plugins/middleware-plugin'
+
+describe('Edge Compiler can import asset assets', () => {
+ let next: NextInstance
+
+ // TODO: remove after this is supported for deploy
+ // if ((global as any).isNextDeploy) {
+ // it('should skip for deploy for now', () => {})
+ // return
+ // }
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: new FileRef(path.join(__dirname, './app')),
+ })
+ })
+ afterAll(() => next.destroy())
+ it('allows to fetch a remote URL', async () => {
+ const response = await fetchViaHTTP(next.url, '/api/edge', {
+ handler: 'remote-full',
+ })
+ expect(await response.text()).toContain('Example Domain')
+ })
+ it('allows to fetch a remote URL with a path and basename', async () => {
+ const response = await fetchViaHTTP(
+ next.url,
+ '/api/edge',
+ {
+ handler: 'remote-with-base',
+ },
+ {
+ compress: true,
+ },
+ )
+ expect(await response.text()).toContain('Example Domain')
+ })
+
+ it('allows to fetch text assets', async () => {
+ const html = await renderViaHTTP(next.url, '/api/edge', {
+ handler: 'text-file',
+ })
+ expect(html).toContain('Hello, from text-file.txt!')
+ })
+ it('allows to fetch image assets', async () => {
+ const response = await fetchViaHTTP(next.url, '/api/edge', {
+ handler: 'image-file',
+ })
+ const buffer: Buffer = await response.buffer()
+ const image = await fs.readFile(path.join(__dirname, './app/src/vercel.png'))
+ expect(buffer.equals(image)).toBeTruthy()
+ })
+
+ // it('allows to assets from node_modules', async () => {
+ // const response = await fetchViaHTTP(next.url, '/api/edge', {
+ // handler: 'from-node-module',
+ // })
+ // const json = await response.json()
+ // expect(json).toEqual({
+ // 'i am': 'a node dependency',
+ // })
+ // })
+
+ it('extracts all the assets from the bundle', async () => {
+ const manifestPath = path.join(next.testDir, '.next/server/middleware-manifest.json')
+ const manifest: MiddlewareManifest = await readJson(manifestPath)
+ const orderedAssets = manifest.functions['/api/edge'].assets.sort((a, z) => {
+ return String(a.name).localeCompare(z.name)
+ })
+
+ expect(orderedAssets).toMatchObject([
+ {
+ name: expect.stringMatching(/^text-file\.[0-9a-f]{16}\.txt$/),
+ filePath: expect.stringMatching(/^server\/edge-chunks\/asset_text-file/),
+ },
+ {
+ name: expect.stringMatching(/^vercel\.[0-9a-f]{16}\.png$/),
+ filePath: expect.stringMatching(/^server\/edge-chunks\/asset_vercel/),
+ },
+ // {
+ // name: expect.stringMatching(/^world\.[0-9a-f]{16}\.json/),
+ // filePath: expect.stringMatching(/^server\/edge-chunks\/asset_world/),
+ // },
+ ])
+ })
+})
diff --git a/test/e2e/modified-tests/i18n-ignore-rewrite-source-locale/rewrites-with-basepath.test.ts b/test/e2e/modified-tests/i18n-ignore-rewrite-source-locale/rewrites-with-basepath.test.ts
new file mode 100644
index 0000000000..9a88d23d34
--- /dev/null
+++ b/test/e2e/modified-tests/i18n-ignore-rewrite-source-locale/rewrites-with-basepath.test.ts
@@ -0,0 +1,76 @@
+import { createNext } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils'
+import path from 'path'
+import fs from 'fs-extra'
+
+const locales = ['', '/en', '/sv', '/nl']
+
+describe('i18n-ignore-rewrite-source-locale with basepath', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ 'pages/api/hello.js': `
+ export default function handler(req, res) {
+ res.send('hello from api')
+ }`,
+ 'public/file.txt': 'hello from file.txt',
+ },
+ dependencies: {},
+ nextConfig: {
+ basePath: '/basepath',
+ i18n: {
+ locales: ['en', 'sv', 'nl'],
+ defaultLocale: 'en',
+ },
+ async rewrites() {
+ return {
+ beforeFiles: [
+ {
+ source: '/:locale/rewrite-files/:path*',
+ destination: '/:path*',
+ locale: false,
+ },
+ {
+ source: '/:locale/rewrite-api/:path*',
+ destination: '/api/:path*',
+ locale: false,
+ },
+ ],
+ afterFiles: [],
+ fallback: [],
+ }
+ },
+ },
+ })
+ })
+ afterAll(() => next.destroy())
+ // NTL Fail
+ test.skip.each(locales)('get public file by skipping locale in rewrite, locale: %s', async (locale) => {
+ const res = await renderViaHTTP(next.url, `/basepath${locale}/rewrite-files/file.txt`)
+ expect(res).toContain('hello from file.txt')
+ })
+
+ test.each(locales)('call api by skipping locale in rewrite, locale: %s', async (locale) => {
+ const res = await renderViaHTTP(next.url, `/basepath${locale}/rewrite-api/hello`)
+ expect(res).toContain('hello from api')
+ })
+
+ // build artifacts aren't available on deploy
+ if (!(global as any).isNextDeploy) {
+ test.each(locales)('get _next/static/ files by skipping locale in rewrite, locale: %s', async (locale) => {
+ const chunks = (await fs.readdir(path.join(next.testDir, '.next', 'static', 'chunks'))).filter((f) =>
+ f.endsWith('.js'),
+ )
+
+ await Promise.all(
+ chunks.map(async (file) => {
+ const res = await fetchViaHTTP(next.url, `/basepath${locale}/rewrite-files/_next/static/chunks/${file}`)
+ expect(res.status).toBe(200)
+ }),
+ )
+ })
+ }
+})
diff --git a/test/e2e/modified-tests/i18n-ignore-rewrite-source-locale/rewrites.test.ts b/test/e2e/modified-tests/i18n-ignore-rewrite-source-locale/rewrites.test.ts
new file mode 100644
index 0000000000..86dfa7b617
--- /dev/null
+++ b/test/e2e/modified-tests/i18n-ignore-rewrite-source-locale/rewrites.test.ts
@@ -0,0 +1,75 @@
+import { createNext } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils'
+import path from 'path'
+import fs from 'fs-extra'
+
+const locales = ['', '/en', '/sv', '/nl']
+
+describe('i18n-ignore-rewrite-source-locale', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ 'pages/api/hello.js': `
+ export default function handler(req, res) {
+ res.send('hello from api')
+ }`,
+ 'public/file.txt': 'hello from file.txt',
+ },
+ dependencies: {},
+ nextConfig: {
+ i18n: {
+ locales: ['en', 'sv', 'nl'],
+ defaultLocale: 'en',
+ },
+ async rewrites() {
+ return {
+ beforeFiles: [
+ {
+ source: '/:locale/rewrite-files/:path*',
+ destination: '/:path*',
+ locale: false,
+ },
+ {
+ source: '/:locale/rewrite-api/:path*',
+ destination: '/api/:path*',
+ locale: false,
+ },
+ ],
+ afterFiles: [],
+ fallback: [],
+ }
+ },
+ },
+ })
+ })
+ afterAll(() => next.destroy())
+ // NTL Fail
+ test.skip.each(locales)('get public file by skipping locale in rewrite, locale: %s', async (locale) => {
+ const res = await renderViaHTTP(next.url, `${locale}/rewrite-files/file.txt`)
+ expect(res).toContain('hello from file.txt')
+ })
+
+ test.each(locales)('call api by skipping locale in rewrite, locale: %s', async (locale) => {
+ const res = await renderViaHTTP(next.url, `${locale}/rewrite-api/hello`)
+ expect(res).toContain('hello from api')
+ })
+
+ // build artifacts aren't available on deploy
+ if (!(global as any).isNextDeploy) {
+ test.each(locales)('get _next/static/ files by skipping locale in rewrite, locale: %s', async (locale) => {
+ const chunks = (await fs.readdir(path.join(next.testDir, '.next', 'static', 'chunks'))).filter((f) =>
+ f.endsWith('.js'),
+ )
+
+ await Promise.all(
+ chunks.map(async (file) => {
+ const res = await fetchViaHTTP(next.url, `${locale}/rewrite-files/_next/static/chunks/${file}`)
+ expect(res.status).toBe(200)
+ }),
+ )
+ })
+ }
+})
diff --git a/test/e2e/modified-tests/middleware-custom-matchers/app/middleware.js b/test/e2e/modified-tests/middleware-custom-matchers/app/middleware.js
new file mode 100644
index 0000000000..99ec191d35
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-custom-matchers/app/middleware.js
@@ -0,0 +1,81 @@
+import { NextResponse } from 'next/server'
+
+export default function middleware(request) {
+ const res = NextResponse.rewrite(new URL('/', request.url))
+ res.headers.set('X-From-Middleware', 'true')
+ return res
+}
+
+export const config = {
+ matcher: [
+ { source: '/source-match' },
+ {
+ source: '/has-match-1',
+ has: [
+ {
+ type: 'header',
+ key: 'x-my-header',
+ value: '(?.*)',
+ },
+ ],
+ },
+ {
+ source: '/has-match-2',
+ has: [
+ {
+ type: 'query',
+ key: 'my-query',
+ },
+ ],
+ },
+ {
+ source: '/has-match-3',
+ has: [
+ {
+ type: 'cookie',
+ key: 'loggedIn',
+ value: '(?true)',
+ },
+ ],
+ },
+ {
+ source: '/has-match-4',
+ has: [
+ {
+ type: 'host',
+ value: 'example.com',
+ },
+ ],
+ },
+ {
+ source: '/has-match-5',
+ has: [
+ {
+ type: 'header',
+ key: 'hasParam',
+ value: 'with-params',
+ },
+ ],
+ },
+ {
+ source: '/missing-match-1',
+ missing: [
+ {
+ type: 'header',
+ key: 'hello',
+ value: '(.*)',
+ },
+ ],
+ },
+ {
+ source: '/missing-match-2',
+ missing: [
+ {
+ type: 'query',
+ key: 'test',
+ value: 'value',
+ },
+ ],
+ },
+ ],
+}
diff --git a/test/e2e/modified-tests/middleware-custom-matchers/app/pages/index.js b/test/e2e/modified-tests/middleware-custom-matchers/app/pages/index.js
new file mode 100644
index 0000000000..accd5b17ad
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-custom-matchers/app/pages/index.js
@@ -0,0 +1,14 @@
+export default (props) => (
+ <>
+ home
+ {props.fromMiddleware}
+ >
+)
+
+export async function getServerSideProps({ res }) {
+ return {
+ props: {
+ fromMiddleware: res.getHeader('x-from-middleware') || null,
+ },
+ }
+}
diff --git a/test/e2e/modified-tests/middleware-custom-matchers/app/pages/routes.js b/test/e2e/modified-tests/middleware-custom-matchers/app/pages/routes.js
new file mode 100644
index 0000000000..9cf902de88
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-custom-matchers/app/pages/routes.js
@@ -0,0 +1,16 @@
+import Link from 'next/link'
+
+export default (props) => (
+
+
+
+ has-match-2
+
+
+
+
+ has-match-3
+
+
+
+)
diff --git a/test/e2e/modified-tests/middleware-custom-matchers/test/index.test.ts b/test/e2e/modified-tests/middleware-custom-matchers/test/index.test.ts
new file mode 100644
index 0000000000..c35aee9c8d
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-custom-matchers/test/index.test.ts
@@ -0,0 +1,157 @@
+/* eslint-env jest */
+/* eslint-disable jest/no-standalone-expect */
+import { join } from 'path'
+import webdriver from 'next-webdriver'
+import { fetchViaHTTP } from 'next-test-utils'
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+const usuallySkip = process.env.RUN_SKIPPED_TESTS ? it : it.skip
+const itif = (condition: boolean) => (condition ? it : it.skip)
+
+const isModeDeploy = process.env.NEXT_TEST_MODE === 'deploy'
+
+describe('Middleware custom matchers', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: new FileRef(join(__dirname, '../app')),
+ })
+ })
+ afterAll(() => next.destroy())
+
+ const runTests = () => {
+ usuallySkip('should match missing header correctly', async () => {
+ const res = await fetchViaHTTP(next.url, '/missing-match-1')
+ expect(res.headers.get('x-from-middleware')).toBeDefined()
+
+ const res2 = await fetchViaHTTP(next.url, '/missing-match-1', undefined, {
+ headers: {
+ hello: 'world',
+ },
+ })
+ expect(res2.headers.get('x-from-middleware')).toBeFalsy()
+ })
+
+ usuallySkip('should match missing query correctly', async () => {
+ const res = await fetchViaHTTP(next.url, '/missing-match-2')
+ expect(res.headers.get('x-from-middleware')).toBeDefined()
+
+ const res2 = await fetchViaHTTP(next.url, '/missing-match-2', {
+ test: 'value',
+ })
+ expect(res2.headers.get('x-from-middleware')).toBeFalsy()
+ })
+
+ it('should match source path', async () => {
+ const res = await fetchViaHTTP(next.url, '/source-match')
+ expect(res.status).toBe(200)
+ expect(res.headers.get('x-from-middleware')).toBeDefined()
+ })
+
+ it('should match has header', async () => {
+ const res = await fetchViaHTTP(next.url, '/has-match-1', undefined, {
+ headers: {
+ 'x-my-header': 'hello world!!',
+ },
+ })
+ expect(res.status).toBe(200)
+ expect(res.headers.get('x-from-middleware')).toBeDefined()
+
+ const res2 = await fetchViaHTTP(next.url, '/has-match-1')
+ expect(res2.status).toBe(404)
+ })
+
+ it('should match has query', async () => {
+ const res = await fetchViaHTTP(next.url, '/has-match-2', {
+ 'my-query': 'hellooo',
+ })
+ expect(res.status).toBe(200)
+ expect(res.headers.get('x-from-middleware')).toBeDefined()
+
+ const res2 = await fetchViaHTTP(next.url, '/has-match-2')
+ expect(res2.status).toBe(404)
+ })
+
+ it('should match has cookie', async () => {
+ const res = await fetchViaHTTP(next.url, '/has-match-3', undefined, {
+ headers: {
+ cookie: 'loggedIn=true',
+ },
+ })
+ expect(res.status).toBe(200)
+ expect(res.headers.get('x-from-middleware')).toBeDefined()
+
+ const res2 = await fetchViaHTTP(next.url, '/has-match-3', undefined, {
+ headers: {
+ cookie: 'loggedIn=false',
+ },
+ })
+ expect(res2.status).toBe(404)
+ })
+
+ // Cannot modify host when testing with real deployment
+ itif(!isModeDeploy)('should match has host', async () => {
+ const res1 = await fetchViaHTTP(next.url, '/has-match-4')
+ expect(res1.status).toBe(404)
+
+ const res = await fetchViaHTTP(next.url, '/has-match-4', undefined, {
+ headers: {
+ host: 'example.com',
+ },
+ })
+
+ expect(res.status).toBe(200)
+ expect(res.headers.get('x-from-middleware')).toBeDefined()
+
+ const res2 = await fetchViaHTTP(next.url, '/has-match-4', undefined, {
+ headers: {
+ host: 'example.org',
+ },
+ })
+ expect(res2.status).toBe(404)
+ })
+
+ it('should match has header value', async () => {
+ const res = await fetchViaHTTP(next.url, '/has-match-5', undefined, {
+ headers: {
+ hasParam: 'with-params',
+ },
+ })
+ expect(res.status).toBe(200)
+ expect(res.headers.get('x-from-middleware')).toBeDefined()
+
+ const res2 = await fetchViaHTTP(next.url, '/has-match-5', undefined, {
+ headers: {
+ hasParam: 'without-params',
+ },
+ })
+ expect(res2.status).toBe(404)
+ })
+
+ // FIXME: Test fails on Vercel deployment for now.
+ // See https://linear.app/vercel/issue/EC-160/header-value-set-on-middleware-is-not-propagated-on-client-request-of
+ itif(!isModeDeploy)('should match has query on client routing', async () => {
+ const browser = await webdriver(next.url, '/routes')
+ await browser.eval('window.__TEST_NO_RELOAD = true')
+ await browser.elementById('has-match-2').click()
+ const fromMiddleware = await browser.elementById('from-middleware').text()
+ expect(fromMiddleware).toBe('true')
+ const noReload = await browser.eval('window.__TEST_NO_RELOAD')
+ expect(noReload).toBe(true)
+ })
+
+ itif(!isModeDeploy)('should match has cookie on client routing', async () => {
+ const browser = await webdriver(next.url, '/routes')
+ await browser.addCookie({ name: 'loggedIn', value: 'true' })
+ await browser.refresh()
+ await browser.eval('window.__TEST_NO_RELOAD = true')
+ await browser.elementById('has-match-3').click()
+ const fromMiddleware = await browser.elementById('from-middleware').text()
+ expect(fromMiddleware).toBe('true')
+ const noReload = await browser.eval('window.__TEST_NO_RELOAD')
+ expect(noReload).toBe(true)
+ })
+ }
+ runTests()
+})
diff --git a/test/e2e/modified-tests/middleware-fetches-with-body/index.test.ts b/test/e2e/modified-tests/middleware-fetches-with-body/index.test.ts
new file mode 100644
index 0000000000..206ffc3e90
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-fetches-with-body/index.test.ts
@@ -0,0 +1,268 @@
+import { createNext } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { fetchViaHTTP } from 'next-test-utils'
+const usuallySkip = process.env.RUN_SKIPPED_TESTS ? it : it.skip
+
+describe('Middleware fetches with body', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ 'pages/api/default.js': `
+ export default (req, res) => res.json({ body: req.body })
+ `,
+ 'pages/api/size_limit_5kb.js': `
+ export const config = { api: { bodyParser: { sizeLimit: '5kb' } } }
+ export default (req, res) => res.json({ body: req.body })
+ `,
+ 'pages/api/size_limit_5mb.js': `
+ export const config = { api: { bodyParser: { sizeLimit: '5mb' } } }
+ export default (req, res) => res.json({ body: req.body })
+ `,
+ 'pages/api/body_parser_false.js': `
+ export const config = { api: { bodyParser: false } }
+
+ async function buffer(readable) {
+ const chunks = []
+ for await (const chunk of readable) {
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)
+ }
+ return Buffer.concat(chunks)
+ }
+
+ export default async (req, res) => {
+ const buf = await buffer(req)
+ const rawBody = buf.toString('utf8');
+
+ res.json({ rawBody, body: req.body })
+ }
+ `,
+ 'middleware.js': `
+ import { NextResponse } from 'next/server';
+
+ export default async (req) => NextResponse.next();
+ `,
+ },
+ dependencies: {},
+ })
+ })
+ afterAll(() => next.destroy())
+
+ describe('with default bodyParser sizeLimit (1mb)', () => {
+ it('should return 413 for body greater than 1mb', async () => {
+ const bodySize = 1024 * 1024 + 1
+ const body = 'r'.repeat(bodySize)
+
+ const res = await fetchViaHTTP(
+ next.url,
+ '/api/default',
+ {},
+ {
+ body,
+ method: 'POST',
+ },
+ )
+
+ expect(res.status).toBe(413)
+
+ if (!(global as any).isNextDeploy) {
+ expect(res.statusText).toBe('Body exceeded 1mb limit')
+ }
+ })
+
+ it('should be able to send and return body size equal to 1mb', async () => {
+ const bodySize = 1024 * 1024
+ const body = 'B1C2D3E4F5G6H7I8J9K0LaMbNcOdPeQf'.repeat(bodySize / 32)
+
+ const res = await fetchViaHTTP(
+ next.url,
+ '/api/default',
+ {},
+ {
+ body,
+ method: 'POST',
+ },
+ )
+ const data = await res.json()
+
+ expect(res.status).toBe(200)
+ expect(data.body.length).toBe(bodySize)
+ expect(data.body.split('B1C2D3E4F5G6H7I8J9K0LaMbNcOdPeQf').length).toBe(bodySize / 32 + 1)
+ })
+
+ it('should be able to send and return body greater than default highWaterMark (16KiB)', async () => {
+ const bodySize = 16 * 1024 + 1
+ const body = 'CD1E2F3G4H5I6J7K8L9M0NaObPcQdReS'.repeat(bodySize / 32) + 'C'
+
+ const res = await fetchViaHTTP(
+ next.url,
+ '/api/default',
+ {},
+ {
+ body,
+ method: 'POST',
+ },
+ )
+ const data = await res.json()
+
+ expect(res.status).toBe(200)
+ expect(data.body.length).toBe(bodySize)
+ expect(data.body.split('CD1E2F3G4H5I6J7K8L9M0NaObPcQdReS').length).toBe(512 + 1)
+ })
+ })
+
+ describe('with custom bodyParser sizeLimit (5kb)', () => {
+ it('should return 413 for body greater than 5kb', async () => {
+ const bodySize = 5 * 1024 + 1
+ const body = 's'.repeat(bodySize)
+
+ const res = await fetchViaHTTP(
+ next.url,
+ '/api/size_limit_5kb',
+ {},
+ {
+ body,
+ method: 'POST',
+ },
+ )
+
+ expect(res.status).toBe(413)
+
+ if (!(global as any).isNextDeploy) {
+ expect(res.statusText).toBe('Body exceeded 5kb limit')
+ }
+ })
+
+ it('should be able to send and return body size equal to 5kb', async () => {
+ const bodySize = 5120
+ const body = 'DEF1G2H3I4J5K6L7M8N9O0PaQbRcSdTe'.repeat(bodySize / 32)
+
+ const res = await fetchViaHTTP(
+ next.url,
+ '/api/size_limit_5kb',
+ {},
+ {
+ body,
+ method: 'POST',
+ },
+ )
+ const data = await res.json()
+
+ expect(res.status).toBe(200)
+ expect(data.body.length).toBe(bodySize)
+ expect(data.body.split('DEF1G2H3I4J5K6L7M8N9O0PaQbRcSdTe').length).toBe(bodySize / 32 + 1)
+ })
+ })
+
+ describe('with custom bodyParser sizeLimit (5mb)', () => {
+ // NTL: disabled because of EF bug
+ usuallySkip('should return 413 for body equal to 10mb', async () => {
+ const bodySize = 10 * 1024 * 1024
+ const body = 't'.repeat(bodySize)
+
+ const res = await fetchViaHTTP(
+ next.url,
+ '/api/size_limit_5mb',
+ {},
+ {
+ body,
+ method: 'POST',
+ },
+ )
+
+ expect(res.status).toBe(413)
+
+ if (!(global as any).isNextDeploy) {
+ expect(res.statusText).toBe('Body exceeded 5mb limit')
+ }
+ })
+
+ it('should return 413 for body greater than 5mb', async () => {
+ const bodySize = 5 * 1024 * 1024 + 1
+ const body = 'u'.repeat(bodySize)
+
+ const res = await fetchViaHTTP(
+ next.url,
+ '/api/size_limit_5mb',
+ {},
+ {
+ body,
+ method: 'POST',
+ },
+ )
+
+ expect(res.status).toBe(413)
+
+ if (!(global as any).isNextDeploy) {
+ expect(res.statusText).toBe('Body exceeded 5mb limit')
+ }
+ })
+
+ if (!(global as any).isNextDeploy) {
+ it('should be able to send and return body size equal to 5mb', async () => {
+ const bodySize = 5 * 1024 * 1024
+ const body = 'FGHI1J2K3L4M5N6O7P8Q9R0SaTbUcVdW'.repeat(bodySize / 32)
+
+ const res = await fetchViaHTTP(
+ next.url,
+ '/api/size_limit_5mb',
+ {},
+ {
+ body,
+ method: 'POST',
+ },
+ )
+ const data = await res.json()
+
+ expect(res.status).toBe(200)
+ expect(data.body.length).toBe(bodySize)
+ expect(data.body.split('FGHI1J2K3L4M5N6O7P8Q9R0SaTbUcVdW').length).toBe(bodySize / 32 + 1)
+ })
+ }
+ })
+
+ describe('with bodyParser = false', () => {
+ it('should be able to send and return with body size equal to 16KiB', async () => {
+ const bodySize = 16 * 1024
+ const body = 'HIJK1L2M3N4O5P6Q7R8S9T0UaVbWcXdY'.repeat(bodySize / 32)
+
+ const res = await fetchViaHTTP(
+ next.url,
+ '/api/body_parser_false',
+ {},
+ {
+ body,
+ method: 'POST',
+ },
+ )
+ const data = await res.json()
+
+ expect(res.status).toBe(200)
+ expect(data.body).toBeUndefined()
+ expect(data.rawBody.length).toBe(bodySize)
+ expect(data.rawBody.split('HIJK1L2M3N4O5P6Q7R8S9T0UaVbWcXdY').length).toBe(bodySize / 32 + 1)
+ })
+
+ it('should be able to send and return with body greater than 16KiB', async () => {
+ const bodySize = 1024 * 1024
+ const body = 'JKLM1N2O3P4Q5R6S7T8U9V0WaXbYcZdA'.repeat(bodySize / 32)
+
+ const res = await fetchViaHTTP(
+ next.url,
+ '/api/body_parser_false',
+ {},
+ {
+ body,
+ method: 'POST',
+ },
+ )
+ const data = await res.json()
+
+ expect(res.status).toBe(200)
+ expect(data.body).toBeUndefined()
+ expect(data.rawBody.length).toBe(bodySize)
+ expect(data.rawBody.split('JKLM1N2O3P4Q5R6S7T8U9V0WaXbYcZdA').length).toBe(bodySize / 32 + 1)
+ })
+ })
+})
diff --git a/test/e2e/modified-tests/middleware-general/app/middleware.js b/test/e2e/modified-tests/middleware-general/app/middleware.js
new file mode 100644
index 0000000000..54e5b196af
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-general/app/middleware.js
@@ -0,0 +1,273 @@
+/* global globalThis */
+import { NextRequest, NextResponse, URLPattern } from 'next/server'
+import magicValue from 'shared-package'
+
+export const config = { regions: 'auto' }
+
+const PATTERNS = [
+ [
+ new URLPattern({ pathname: '/:locale/:id' }),
+ ({ pathname }) => ({
+ pathname: '/:locale/:id',
+ params: pathname.groups,
+ }),
+ ],
+ [
+ new URLPattern({ pathname: '/:id' }),
+ ({ pathname }) => ({
+ pathname: '/:id',
+ params: pathname.groups,
+ }),
+ ],
+]
+
+const params = (url) => {
+ const input = url.split('?')[0]
+ let result = {}
+
+ for (const [pattern, handler] of PATTERNS) {
+ const patternResult = pattern.exec(input)
+ if (patternResult !== null && 'pathname' in patternResult) {
+ result = handler(patternResult)
+ break
+ }
+ }
+ return result
+}
+
+export async function middleware(request) {
+ const url = request.nextUrl
+
+ if (request.headers.get('x-prerender-revalidate')) {
+ return NextResponse.next({
+ headers: { 'x-middleware': 'hi' },
+ })
+ }
+
+ // this is needed for tests to get the BUILD_ID
+ if (url.pathname.startsWith('/_next/static/__BUILD_ID')) {
+ return NextResponse.next()
+ }
+
+ if (url.pathname === '/api/edge-search-params') {
+ const newUrl = url.clone()
+ newUrl.searchParams.set('foo', 'bar')
+ return NextResponse.rewrite(newUrl)
+ }
+
+ if (url.pathname === '/') {
+ url.pathname = '/ssg/first'
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname === '/to-ssg') {
+ url.pathname = '/ssg/hello'
+ url.searchParams.set('from', 'middleware')
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname === '/sha') {
+ url.pathname = '/shallow'
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname.startsWith('/fetch-user-agent-default')) {
+ try {
+ const apiRoute = new URL(url)
+ apiRoute.pathname = '/api/headers'
+ const res = await fetch(withLocalIp(apiRoute))
+ return serializeData(await res.text())
+ } catch (err) {
+ return serializeError(err)
+ }
+ }
+
+ if (url.pathname === '/rewrite-to-dynamic') {
+ url.pathname = '/blog/from-middleware'
+ url.searchParams.set('some', 'middleware')
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname === '/rewrite-to-config-rewrite') {
+ url.pathname = '/rewrite-3'
+ url.searchParams.set('some', 'middleware')
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname.startsWith('/fetch-user-agent-crypto')) {
+ try {
+ const apiRoute = new URL(url)
+ apiRoute.pathname = '/api/headers'
+ const res = await fetch(withLocalIp(apiRoute), {
+ headers: {
+ 'user-agent': 'custom-agent',
+ },
+ })
+ return serializeData(await res.text())
+ } catch (err) {
+ return serializeError(err)
+ }
+ }
+
+ if (url.pathname === '/global') {
+ // The next line is required to allow to find the env variable
+ // eslint-disable-next-line no-unused-expressions
+ process.env.MIDDLEWARE_TEST
+
+ // The next line is required to allow to find the env variable
+ // eslint-disable-next-line no-unused-expressions
+ const { ANOTHER_MIDDLEWARE_TEST } = process.env
+ if (!ANOTHER_MIDDLEWARE_TEST) {
+ console.log('missing ANOTHER_MIDDLEWARE_TEST')
+ }
+
+ const { STRING_ENV_VAR: stringEnvVar } = process['env']
+ if (!stringEnvVar) {
+ console.log('missing STRING_ENV_VAR')
+ }
+
+ return serializeData(JSON.stringify({ process: { env: process.env } }))
+ }
+
+ if (url.pathname.endsWith('/globalthis')) {
+ return serializeData(JSON.stringify(Object.keys(globalThis)))
+ }
+
+ if (url.pathname.endsWith('/webcrypto')) {
+ const response = {}
+ try {
+ const algorithm = {
+ name: 'RSA-PSS',
+ hash: 'SHA-256',
+ publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
+ modulusLength: 2048,
+ }
+ const keyUsages = ['sign', 'verify']
+ await crypto.subtle.generateKey(algorithm, false, keyUsages)
+ } catch (err) {
+ response.error = true
+ } finally {
+ return serializeData(JSON.stringify(response))
+ }
+ }
+
+ if (url.pathname.endsWith('/fetch-url')) {
+ const response = {}
+ try {
+ await fetch(new URL('http://localhost'))
+ } catch (err) {
+ response.error = {
+ name: err.name,
+ message: err.message,
+ }
+ } finally {
+ return serializeData(JSON.stringify(response))
+ }
+ }
+
+ if (url.pathname === '/abort-controller') {
+ const controller = new AbortController()
+ const signal = controller.signal
+
+ controller.abort()
+ const response = {}
+
+ try {
+ await fetch('https://example.vercel.sh', { signal })
+ } catch (err) {
+ response.error = {
+ name: err.name,
+ message: err.message,
+ }
+ } finally {
+ return serializeData(JSON.stringify(response))
+ }
+ }
+
+ if (url.pathname.endsWith('/root-subrequest')) {
+ const res = await fetch(url)
+ res.headers.set('x-dynamic-path', 'true')
+ return res
+ }
+
+ if (url.pathname === '/about') {
+ if (magicValue !== 42) throw new Error('shared-package problem')
+ return NextResponse.rewrite(new URL('/about/a', request.url))
+ }
+
+ if (url.pathname === '/redirect-to-somewhere') {
+ url.pathname = '/somewhere'
+ return NextResponse.redirect(url, {
+ headers: {
+ 'x-redirect-header': 'hi',
+ },
+ })
+ }
+
+ if (url.pathname.startsWith('/url')) {
+ try {
+ if (request.nextUrl.pathname === '/url/relative-url') {
+ new URL('/relative')
+ return Response.next()
+ }
+
+ if (request.nextUrl.pathname === '/url/relative-request') {
+ await fetch(new Request('/urls-b'))
+ return Response.next()
+ }
+
+ if (request.nextUrl.pathname === '/url/relative-redirect') {
+ return Response.redirect('/urls-b')
+ }
+
+ if (request.nextUrl.pathname === '/url/relative-next-redirect') {
+ return NextResponse.redirect('/urls-b')
+ }
+
+ if (request.nextUrl.pathname === '/url/relative-next-rewrite') {
+ return NextResponse.rewrite('/urls-b')
+ }
+
+ if (request.nextUrl.pathname === '/url/relative-next-request') {
+ await fetch(new NextRequest('/urls-b'))
+ return NextResponse.next()
+ }
+ } catch (error) {
+ return new NextResponse(null, { headers: { error: error.message } })
+ }
+ }
+
+ if (url.pathname === '/ssr-page') {
+ url.pathname = '/ssr-page-2'
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname === '/error-throw' && request.__isData) {
+ throw new Error('test error')
+ }
+
+ const original = new URL(request.url)
+ return NextResponse.next({
+ headers: {
+ 'req-url-path': `${original.pathname}${original.search}`,
+ 'req-url-basepath': request.nextUrl.basePath,
+ 'req-url-pathname': request.nextUrl.pathname,
+ 'req-url-query': request.nextUrl.searchParams.get('foo'),
+ 'req-url-locale': request.nextUrl.locale,
+ 'req-url-params':
+ url.pathname !== '/static' ? JSON.stringify(params(request.url)) : '{}',
+ },
+ })
+}
+
+function serializeData(data) {
+ return new NextResponse(null, { headers: { data } })
+}
+
+function serializeError(error) {
+ return new NextResponse(null, { headers: { error: error.message } })
+}
+
+function withLocalIp(url) {
+ return String(url).replace('localhost', '127.0.0.1')
+}
diff --git a/test/e2e/modified-tests/middleware-general/app/next.config.js b/test/e2e/modified-tests/middleware-general/app/next.config.js
new file mode 100644
index 0000000000..9308df9dcb
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-general/app/next.config.js
@@ -0,0 +1,35 @@
+module.exports = {
+ i18n: {
+ locales: ['en', 'fr', 'nl'],
+ defaultLocale: 'en',
+ },
+ redirects() {
+ return [
+ {
+ source: '/redirect-1',
+ destination: '/somewhere/else',
+ permanent: false,
+ },
+ ]
+ },
+ rewrites() {
+ return [
+ {
+ source: '/rewrite-1',
+ destination: '/ssr-page?from=config',
+ },
+ {
+ source: '/rewrite-2',
+ destination: '/about/a?from=next-config',
+ },
+ {
+ source: '/sha',
+ destination: '/shallow',
+ },
+ {
+ source: '/rewrite-3',
+ destination: '/blog/middleware-rewrite?hello=config',
+ },
+ ]
+ },
+}
diff --git a/test/e2e/modified-tests/middleware-general/app/node_modules/shared-package/index.js b/test/e2e/modified-tests/middleware-general/app/node_modules/shared-package/index.js
new file mode 100644
index 0000000000..a8653a9c92
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-general/app/node_modules/shared-package/index.js
@@ -0,0 +1 @@
+module.exports = 42
diff --git a/test/e2e/modified-tests/middleware-general/app/node_modules/shared-package/package.json b/test/e2e/modified-tests/middleware-general/app/node_modules/shared-package/package.json
new file mode 100644
index 0000000000..1587a66968
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-general/app/node_modules/shared-package/package.json
@@ -0,0 +1,3 @@
+{
+ "version": "1.0.0"
+}
diff --git a/test/e2e/modified-tests/middleware-general/app/pages/[id].js b/test/e2e/modified-tests/middleware-general/app/pages/[id].js
new file mode 100644
index 0000000000..11f4614a67
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-general/app/pages/[id].js
@@ -0,0 +1,3 @@
+export default function Index() {
+ return Dynamic route
+}
diff --git a/test/e2e/modified-tests/middleware-general/app/pages/_app.js b/test/e2e/modified-tests/middleware-general/app/pages/_app.js
new file mode 100644
index 0000000000..65e905445e
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-general/app/pages/_app.js
@@ -0,0 +1,8 @@
+export default function App({ Component, pageProps }) {
+ if (!pageProps || typeof pageProps !== 'object') {
+ throw new Error(
+ `Invariant: received invalid pageProps in _app, received ${pageProps}`
+ )
+ }
+ return
+}
diff --git a/test/e2e/modified-tests/middleware-general/app/pages/about/a.js b/test/e2e/modified-tests/middleware-general/app/pages/about/a.js
new file mode 100644
index 0000000000..dfacd4eaff
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-general/app/pages/about/a.js
@@ -0,0 +1,7 @@
+export default function AboutA() {
+ return (
+
+
AboutA
+
+ )
+}
diff --git a/test/e2e/modified-tests/middleware-general/app/pages/about/b.js b/test/e2e/modified-tests/middleware-general/app/pages/about/b.js
new file mode 100644
index 0000000000..7a44254c45
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-general/app/pages/about/b.js
@@ -0,0 +1,7 @@
+export default function AboutB() {
+ return (
+
+
AboutB
+
+ )
+}
diff --git a/test/e2e/modified-tests/middleware-general/app/pages/api/edge-search-params.js b/test/e2e/modified-tests/middleware-general/app/pages/api/edge-search-params.js
new file mode 100644
index 0000000000..01a968ee9d
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-general/app/pages/api/edge-search-params.js
@@ -0,0 +1,10 @@
+import { NextResponse } from 'next/server'
+
+export const config = { runtime: 'experimental-edge', regions: 'default' }
+
+/**
+ * @param {import('next/server').NextRequest}
+ */
+export default (req) => {
+ return NextResponse.json(Object.fromEntries(req.nextUrl.searchParams))
+}
diff --git a/test/e2e/modified-tests/middleware-general/app/pages/api/headers.js b/test/e2e/modified-tests/middleware-general/app/pages/api/headers.js
new file mode 100644
index 0000000000..0f65c82e9f
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-general/app/pages/api/headers.js
@@ -0,0 +1,3 @@
+export default function handler(req, res) {
+ res.json({ url: req.url, headers: req.headers })
+}
diff --git a/test/e2e/modified-tests/middleware-general/app/pages/blog/[slug].js b/test/e2e/modified-tests/middleware-general/app/pages/blog/[slug].js
new file mode 100644
index 0000000000..590ca0dcc1
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-general/app/pages/blog/[slug].js
@@ -0,0 +1,23 @@
+import { useRouter } from 'next/router'
+
+export default function Page(props) {
+ const router = useRouter()
+ return (
+ <>
+ /blog/[slug]
+ {JSON.stringify(router.query)}
+ {router.pathname}
+ {router.asPath}
+ {JSON.stringify(props)}
+ >
+ )
+}
+
+export function getServerSideProps({ params }) {
+ return {
+ props: {
+ now: Date.now(),
+ params,
+ },
+ }
+}
diff --git a/test/e2e/modified-tests/middleware-general/app/pages/error-throw.js b/test/e2e/modified-tests/middleware-general/app/pages/error-throw.js
new file mode 100644
index 0000000000..b8a8706e5f
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-general/app/pages/error-throw.js
@@ -0,0 +1,12 @@
+export default function ThrowOnData({ message }) {
+ return (
+
+
Throw on data request
+
{message}
+
+ )
+}
+
+export const getServerSideProps = ({ query }) => ({
+ props: { message: query.message || '' },
+})
diff --git a/test/e2e/modified-tests/middleware-general/app/pages/error.js b/test/e2e/modified-tests/middleware-general/app/pages/error.js
new file mode 100644
index 0000000000..a5a49734cc
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-general/app/pages/error.js
@@ -0,0 +1,11 @@
+import Link from 'next/link'
+
+export default function Errors() {
+ return (
+
+
+ Throw on data
+
+
+ )
+}
diff --git a/test/e2e/modified-tests/middleware-general/app/pages/shallow.js b/test/e2e/modified-tests/middleware-general/app/pages/shallow.js
new file mode 100644
index 0000000000..4d1c8564ee
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-general/app/pages/shallow.js
@@ -0,0 +1,43 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+export default function Shallow({ message }) {
+ const { pathname, query } = useRouter()
+ return (
+
+
+ {message}
+
+
+ Shallow link to ?hello=world
+
+
+
+
+ Deep link to ?hello=goodbye
+
+
+
+
+ Current path: {pathname}
+
+
+
+
+ Current query: {JSON.stringify(query)}
+
+
+
+
+ )
+}
+
+let i = 0
+
+export const getServerSideProps = () => {
+ return {
+ props: {
+ message: `Random: ${++i}${Math.random()}`,
+ },
+ }
+}
diff --git a/test/e2e/modified-tests/middleware-general/app/pages/ssg/[slug].js b/test/e2e/modified-tests/middleware-general/app/pages/ssg/[slug].js
new file mode 100644
index 0000000000..cf9fb1db0b
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-general/app/pages/ssg/[slug].js
@@ -0,0 +1,42 @@
+import { useRouter } from 'next/router'
+import { useEffect } from 'react'
+import { useState } from 'react'
+
+export default function Page(props) {
+ const router = useRouter()
+ const [asPath, setAsPath] = useState(
+ router.isReady ? router.asPath : router.href
+ )
+
+ useEffect(() => {
+ if (router.isReady) {
+ setAsPath(router.asPath)
+ }
+ }, [router.asPath, router.isReady])
+
+ return (
+ <>
+ /blog/[slug]
+ {JSON.stringify(router.query)}
+ {router.pathname}
+ {asPath}
+ {JSON.stringify(props)}
+ >
+ )
+}
+
+export function getStaticProps({ params }) {
+ return {
+ props: {
+ now: Date.now(),
+ params,
+ },
+ }
+}
+
+export function getStaticPaths() {
+ return {
+ paths: ['/ssg/first', '/ssg/hello'],
+ fallback: 'blocking',
+ }
+}
diff --git a/test/e2e/modified-tests/middleware-general/app/pages/ssr-page-2.js b/test/e2e/modified-tests/middleware-general/app/pages/ssr-page-2.js
new file mode 100644
index 0000000000..67e1b70868
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-general/app/pages/ssr-page-2.js
@@ -0,0 +1,11 @@
+export default function SSRPage(props) {
+ return {props.message}
+}
+
+export const getServerSideProps = (req) => {
+ return {
+ props: {
+ message: 'Bye Cruel World',
+ },
+ }
+}
diff --git a/test/e2e/modified-tests/middleware-general/app/pages/ssr-page.js b/test/e2e/modified-tests/middleware-general/app/pages/ssr-page.js
new file mode 100644
index 0000000000..e2aaaa56b4
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-general/app/pages/ssr-page.js
@@ -0,0 +1,11 @@
+export default function SSRPage(props) {
+ return {props.message}
+}
+
+export const getServerSideProps = (req) => {
+ return {
+ props: {
+ message: 'Hello World',
+ },
+ }
+}
diff --git a/test/e2e/modified-tests/middleware-general/test/index.test.ts b/test/e2e/modified-tests/middleware-general/test/index.test.ts
new file mode 100644
index 0000000000..56af39e645
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-general/test/index.test.ts
@@ -0,0 +1,566 @@
+/* eslint-env jest */
+
+import fs from 'fs-extra'
+import { join } from 'path'
+import webdriver from 'next-webdriver'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { check, fetchViaHTTP, waitFor } from 'next-test-utils'
+import { createNext, FileRef } from 'e2e-utils'
+
+const urlsError = 'Please use only absolute URLs'
+const usuallySkip = process.env.RUN_SKIPPED_TESTS ? it : it.skip
+
+describe('Middleware Runtime', () => {
+ let next: NextInstance
+
+ const setup = ({ i18n }: { i18n: boolean }) => {
+ let nextConfigContent = ''
+ const nextConfigPath = join(__dirname, '../app/next.config.js')
+
+ afterAll(async () => {
+ await next.destroy()
+
+ if (nextConfigContent) {
+ await fs.writeFile(nextConfigPath, nextConfigContent)
+ }
+ })
+ beforeAll(async () => {
+ if (!i18n) {
+ nextConfigContent = await fs.readFile(nextConfigPath, 'utf8')
+ await fs.writeFile(nextConfigPath, nextConfigContent.replace('i18n', '__i18n'))
+ }
+ next = await createNext({
+ files: {
+ 'next.config.js': new FileRef(join(__dirname, '../app/next.config.js')),
+ 'middleware.js': new FileRef(join(__dirname, '../app/middleware.js')),
+ pages: new FileRef(join(__dirname, '../app/pages')),
+ 'shared-package': new FileRef(join(__dirname, '../app/node_modules/shared-package')),
+ },
+ packageJson: {
+ scripts: {
+ setup: `cp -r ./shared-package ./node_modules`,
+ build: 'yarn setup && next build',
+ dev: 'yarn setup && next dev',
+ start: 'next start',
+ },
+ },
+ startCommand: (global as any).isNextDev ? 'yarn dev' : 'yarn start',
+ buildCommand: 'yarn build',
+ env: {
+ ANOTHER_MIDDLEWARE_TEST: 'asdf2',
+ STRING_ENV_VAR: 'asdf3',
+ MIDDLEWARE_TEST: 'asdf',
+ },
+ })
+ })
+ }
+
+ function readMiddlewareJSON(response) {
+ return JSON.parse(response.headers.get('data'))
+ }
+
+ function readMiddlewareError(response) {
+ return response.headers.get('error')
+ }
+
+ function runTests({ i18n }: { i18n?: boolean }) {
+ if ((global as any).isNextDev) {
+ it('refreshes the page when middleware changes ', async () => {
+ const browser = await webdriver(next.url, `/about`)
+ await browser.eval('window.didrefresh = "hello"')
+ const text = await browser.elementByCss('h1').text()
+ expect(text).toEqual('AboutA')
+
+ const middlewarePath = join(next.testDir, '/middleware.js')
+ const originalContent = fs.readFileSync(middlewarePath, 'utf-8')
+ const editedContent = originalContent.replace('/about/a', '/about/b')
+
+ try {
+ fs.writeFileSync(middlewarePath, editedContent)
+ await waitFor(1000)
+ const textb = await browser.elementByCss('h1').text()
+ expect(await browser.eval('window.itdidnotrefresh')).not.toBe('hello')
+ expect(textb).toEqual('AboutB')
+ } finally {
+ fs.writeFileSync(middlewarePath, originalContent)
+ await browser.close()
+ }
+ })
+
+ it('should only contain middleware route in dev middleware manifest', async () => {
+ const res = await fetchViaHTTP(next.url, `/_next/static/${next.buildId}/_devMiddlewareManifest.json`)
+ const matchers = await res.json()
+ expect(matchers).toEqual([{ regexp: '.*' }])
+ })
+ }
+
+ if ((global as any).isNextStart) {
+ it('should have valid middleware field in manifest', async () => {
+ const manifest = await fs.readJSON(join(next.testDir, '.next/server/middleware-manifest.json'))
+ expect(manifest.middleware).toEqual({
+ '/': {
+ env: ['MIDDLEWARE_TEST', 'ANOTHER_MIDDLEWARE_TEST', 'STRING_ENV_VAR'],
+ files: expect.arrayContaining(['server/edge-runtime-webpack.js', 'server/middleware.js']),
+ name: 'middleware',
+ page: '/',
+ matchers: [{ regexp: '^/.*$' }],
+ wasm: [],
+ assets: [],
+ regions: 'auto',
+ },
+ })
+ })
+
+ it('should have the custom config in the manifest', async () => {
+ const manifest = await fs.readJSON(join(next.testDir, '.next/server/middleware-manifest.json'))
+
+ expect(manifest.functions['/api/edge-search-params']).toHaveProperty('regions', 'default')
+ })
+
+ it('should have correct files in manifest', async () => {
+ const manifest = await fs.readJSON(join(next.testDir, '.next/server/middleware-manifest.json'))
+ for (const key of Object.keys(manifest.middleware)) {
+ const middleware = manifest.middleware[key]
+ expect(middleware.files).toContainEqual(expect.stringContaining('server/edge-runtime-webpack'))
+ expect(middleware.files).not.toContainEqual(expect.stringContaining('static/chunks/'))
+ }
+ })
+
+ it('should not run middleware for on-demand revalidate', async () => {
+ const bypassToken = (await fs.readJSON(join(next.testDir, '.next/prerender-manifest.json'))).preview
+ .previewModeId
+
+ const res = await fetchViaHTTP(next.url, '/ssg/first', undefined, {
+ headers: {
+ 'x-prerender-revalidate': bypassToken,
+ },
+ })
+ expect(res.status).toBe(200)
+ expect(res.headers.get('x-middleware')).toBeFalsy()
+ expect(res.headers.get('x-nextjs-cache')).toBe('REVALIDATED')
+ })
+ }
+ // NTL Fail
+ usuallySkip('passes search params with rewrites', async () => {
+ const response = await fetchViaHTTP(next.url, `/api/edge-search-params`, {
+ a: 'b',
+ })
+ await expect(response.json()).resolves.toMatchObject({
+ a: 'b',
+ // included from middleware
+ foo: 'bar',
+ })
+ })
+
+ it('should have init header for NextResponse.redirect', async () => {
+ const res = await fetchViaHTTP(next.url, '/redirect-to-somewhere', undefined, {
+ redirect: 'manual',
+ })
+ expect(res.status).toBe(307)
+ expect(new URL(res.headers.get('location'), 'http://n').pathname).toBe('/somewhere')
+ expect(res.headers.get('x-redirect-header')).toBe('hi')
+ })
+
+ it('should have correct query values for rewrite to ssg page', async () => {
+ const browser = await webdriver(next.url, '/to-ssg', {
+ waitHydration: false,
+ })
+ const requests = []
+
+ browser.on('request', (req) => {
+ console.error('request', req.url(), req.method())
+ if (req.method() === 'HEAD') {
+ requests.push(req.url())
+ }
+ })
+ await browser.eval('window.beforeNav = 1')
+
+ await check(async () => {
+ const didReq = await browser.eval('next.router.isReady')
+ return didReq || requests.some((req) => new URL(req, 'http://n').pathname.endsWith('/to-ssg.json'))
+ ? 'found'
+ : JSON.stringify(requests)
+ }, 'found')
+
+ await check(() => browser.eval('document.documentElement.innerHTML'), /"slug":"hello"/)
+
+ await check(() => browser.elementByCss('body').text(), /\/to-ssg/)
+
+ expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
+ slug: 'hello',
+ })
+ expect(JSON.parse(await browser.elementByCss('#props').text()).params).toEqual({
+ slug: 'hello',
+ })
+ expect(await browser.elementByCss('#pathname').text()).toBe('/ssg/[slug]')
+ expect(await browser.elementByCss('#as-path').text()).toBe('/to-ssg')
+ })
+
+ it('should have correct dynamic route params on client-transition to dynamic route', async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.eval('window.beforeNav = 1')
+ await browser.eval('window.next.router.push("/blog/first")')
+ await browser.waitForElementByCss('#blog')
+
+ expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
+ slug: 'first',
+ })
+ expect(JSON.parse(await browser.elementByCss('#props').text()).params).toEqual({
+ slug: 'first',
+ })
+ expect(await browser.elementByCss('#pathname').text()).toBe('/blog/[slug]')
+ expect(await browser.elementByCss('#as-path').text()).toBe('/blog/first')
+
+ await browser.eval('window.next.router.push("/blog/second")')
+ await check(() => browser.elementByCss('body').text(), /"slug":"second"/)
+
+ expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
+ slug: 'second',
+ })
+ expect(JSON.parse(await browser.elementByCss('#props').text()).params).toEqual({
+ slug: 'second',
+ })
+ expect(await browser.elementByCss('#pathname').text()).toBe('/blog/[slug]')
+ expect(await browser.elementByCss('#as-path').text()).toBe('/blog/second')
+ })
+
+ it('should have correct dynamic route params for middleware rewrite to dynamic route', async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.eval('window.beforeNav = 1')
+ await browser.eval('window.next.router.push("/rewrite-to-dynamic")')
+ await browser.waitForElementByCss('#blog')
+
+ expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
+ slug: 'from-middleware',
+ some: 'middleware',
+ })
+ expect(JSON.parse(await browser.elementByCss('#props').text()).params).toEqual({
+ slug: 'from-middleware',
+ })
+ expect(await browser.elementByCss('#pathname').text()).toBe('/blog/[slug]')
+ expect(await browser.elementByCss('#as-path').text()).toBe('/rewrite-to-dynamic')
+ })
+
+ it('should have correct route params for chained rewrite from middleware to config rewrite', async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.eval('window.beforeNav = 1')
+ await browser.eval('window.next.router.push("/rewrite-to-config-rewrite")')
+ await browser.waitForElementByCss('#blog')
+
+ expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
+ slug: 'middleware-rewrite',
+ hello: 'config',
+ some: 'middleware',
+ })
+ expect(JSON.parse(await browser.elementByCss('#props').text()).params).toEqual({
+ slug: 'middleware-rewrite',
+ })
+ expect(await browser.elementByCss('#pathname').text()).toBe('/blog/[slug]')
+ expect(await browser.elementByCss('#as-path').text()).toBe('/rewrite-to-config-rewrite')
+ })
+ // NTL Fail
+ usuallySkip('should have correct route params for rewrite from config dynamic route', async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.eval('window.beforeNav = 1')
+ await browser.eval('window.next.router.push("/rewrite-3")')
+ await browser.waitForElementByCss('#blog')
+
+ expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
+ slug: 'middleware-rewrite',
+ hello: 'config',
+ })
+ expect(JSON.parse(await browser.elementByCss('#props').text()).params).toEqual({
+ slug: 'middleware-rewrite',
+ })
+ expect(await browser.elementByCss('#pathname').text()).toBe('/blog/[slug]')
+ expect(await browser.elementByCss('#as-path').text()).toBe('/rewrite-3')
+ })
+ // NTL Fail
+ usuallySkip('should have correct route params for rewrite from config non-dynamic route', async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.eval('window.beforeNav = 1')
+ await browser.eval('window.next.router.push("/rewrite-1")')
+
+ await check(() => browser.eval('document.documentElement.innerHTML'), /Hello World/)
+
+ expect(await browser.eval('window.next.router.query')).toEqual({
+ from: 'config',
+ })
+ })
+ // NTL Fail
+ usuallySkip('should redirect the same for direct visit and client-transition', async () => {
+ const res = await fetchViaHTTP(next.url, `/redirect-1`, undefined, {
+ redirect: 'manual',
+ })
+ expect(res.status).toBe(307)
+ expect(new URL(res.headers.get('location'), 'http://n').pathname).toBe('/somewhere/else')
+
+ const browser = await webdriver(next.url, `/`)
+ await browser.eval(`next.router.push('/redirect-1')`)
+ await check(async () => {
+ const pathname = await browser.eval('location.pathname')
+ return pathname === '/somewhere/else' ? 'success' : pathname
+ }, 'success')
+ })
+ // NTL Fail
+ usuallySkip('should rewrite the same for direct visit and client-transition', async () => {
+ const res = await fetchViaHTTP(next.url, `/rewrite-1`)
+ expect(res.status).toBe(200)
+ expect(await res.text()).toContain('Hello World')
+
+ const browser = await webdriver(next.url, `/`)
+ await browser.eval('window.beforeNav = 1')
+ await browser.eval(`next.router.push('/rewrite-1')`)
+ await check(async () => {
+ const content = await browser.eval('document.documentElement.innerHTML')
+ return content.includes('Hello World') ? 'success' : content
+ }, 'success')
+ expect(await browser.eval('window.beforeNav')).toBe(1)
+ })
+ // NTL Fail
+ usuallySkip('should rewrite correctly for non-SSG/SSP page', async () => {
+ const res = await fetchViaHTTP(next.url, `/rewrite-2`)
+ expect(res.status).toBe(200)
+ expect(await res.text()).toContain('AboutA')
+
+ const browser = await webdriver(next.url, `/`)
+ await browser.eval(`next.router.push('/rewrite-2')`)
+ await check(async () => {
+ const content = await browser.eval('document.documentElement.innerHTML')
+ return content.includes('AboutA') ? 'success' : content
+ }, 'success')
+ })
+
+ it('should respond with 400 on decode failure', async () => {
+ const res = await fetchViaHTTP(next.url, `/%2`)
+ expect(res.status).toBe(400)
+
+ if ((global as any).isNextStart) {
+ expect(await res.text()).toContain('Bad Request')
+ }
+ })
+
+ if (!(global as any).isNextDeploy) {
+ // user agent differs on Vercel
+ it('should set fetch user agent correctly', async () => {
+ const res = await fetchViaHTTP(next.url, `/fetch-user-agent-default`)
+
+ expect(readMiddlewareJSON(res).headers['user-agent']).toBe('Next.js Middleware')
+
+ const res2 = await fetchViaHTTP(next.url, `/fetch-user-agent-crypto`)
+ expect(readMiddlewareJSON(res2).headers['user-agent']).toBe('custom-agent')
+ })
+ }
+ // NTL Fail
+ usuallySkip('should contain process polyfill', async () => {
+ const res = await fetchViaHTTP(next.url, `/global`)
+ expect(readMiddlewareJSON(res)).toEqual({
+ process: {
+ env: {
+ ANOTHER_MIDDLEWARE_TEST: 'asdf2',
+ STRING_ENV_VAR: 'asdf3',
+ MIDDLEWARE_TEST: 'asdf',
+ ...((global as any).isNextDeploy
+ ? {}
+ : {
+ NEXT_RUNTIME: 'edge',
+ }),
+ },
+ },
+ })
+ })
+
+ it(`should contain \`globalThis\``, async () => {
+ const res = await fetchViaHTTP(next.url, '/globalthis')
+ expect(readMiddlewareJSON(res).length > 0).toBe(true)
+ })
+
+ it(`should contain crypto APIs`, async () => {
+ const res = await fetchViaHTTP(next.url, '/webcrypto')
+ expect('error' in readMiddlewareJSON(res)).toBe(false)
+ })
+
+ if (!(global as any).isNextDeploy) {
+ it(`should accept a URL instance for fetch`, async () => {
+ const response = await fetchViaHTTP(next.url, '/fetch-url')
+ // TODO: why is an error expected here if it should work?
+ const { error } = readMiddlewareJSON(response)
+ expect(error).toBeTruthy()
+ expect(error.message).not.toContain("Failed to construct 'URL'")
+ })
+ }
+ // Fail
+ usuallySkip(`should allow to abort a fetch request`, async () => {
+ const response = await fetchViaHTTP(next.url, '/abort-controller')
+ const payload = readMiddlewareJSON(response)
+ expect('error' in payload).toBe(true)
+ expect(payload.error.name).toBe('AbortError')
+ expect(payload.error.message).toContain('The operation was aborted')
+ })
+
+ it(`should validate & parse request url from any route`, async () => {
+ const res = await fetchViaHTTP(next.url, `/static`)
+
+ expect(res.headers.get('req-url-basepath')).toBeFalsy()
+ expect(res.headers.get('req-url-pathname')).toBe('/static')
+
+ const { pathname, params } = JSON.parse(res.headers.get('req-url-params'))
+ expect(pathname).toBe(undefined)
+ expect(params).toEqual(undefined)
+
+ expect(res.headers.get('req-url-query')).not.toBe('bar')
+ })
+
+ if (i18n) {
+ it(`should validate & parse request url from a dynamic route with params`, async () => {
+ const res = await fetchViaHTTP(next.url, `/fr/1`)
+
+ expect(res.headers.get('req-url-basepath')).toBeFalsy()
+ expect(res.headers.get('req-url-pathname')).toBe('/1')
+
+ const { pathname, params } = JSON.parse(res.headers.get('req-url-params'))
+ expect(pathname).toBe('/:locale/:id')
+ expect(params).toEqual({ locale: 'fr', id: '1' })
+
+ expect(res.headers.get('req-url-query')).not.toBe('bar')
+ expect(res.headers.get('req-url-locale')).toBe('fr')
+ })
+
+ it(`should validate & parse request url from a dynamic route with params and no query`, async () => {
+ const res = await fetchViaHTTP(next.url, `/fr/abc123`)
+ expect(res.headers.get('req-url-basepath')).toBeFalsy()
+
+ const { pathname, params } = JSON.parse(res.headers.get('req-url-params'))
+ expect(pathname).toBe('/:locale/:id')
+ expect(params).toEqual({ locale: 'fr', id: 'abc123' })
+
+ expect(res.headers.get('req-url-query')).not.toBe('bar')
+ expect(res.headers.get('req-url-locale')).toBe('fr')
+ })
+ }
+
+ it(`should validate & parse request url from a dynamic route with params and query`, async () => {
+ const res = await fetchViaHTTP(next.url, `/abc123?foo=bar`)
+ expect(res.headers.get('req-url-basepath')).toBeFalsy()
+
+ const { pathname, params } = JSON.parse(res.headers.get('req-url-params'))
+
+ expect(pathname).toBe('/:id')
+ expect(params).toEqual({ id: 'abc123' })
+
+ expect(res.headers.get('req-url-query')).toBe('bar')
+
+ if (i18n) {
+ expect(res.headers.get('req-url-locale')).toBe('en')
+ }
+ })
+
+ it('should throw when using URL with a relative URL', async () => {
+ const res = await fetchViaHTTP(next.url, `/url/relative-url`)
+ expect(readMiddlewareError(res)).toContain('Invalid URL')
+ })
+
+ it('should throw when using NextRequest with a relative URL', async () => {
+ const response = await fetchViaHTTP(next.url, `/url/relative-next-request`)
+ expect(readMiddlewareError(response)).toContain(urlsError)
+ })
+
+ if (!(global as any).isNextDeploy) {
+ // these errors differ on Vercel
+ it('should throw when using Request with a relative URL', async () => {
+ const response = await fetchViaHTTP(next.url, `/url/relative-request`)
+ expect(readMiddlewareError(response)).toContain(urlsError)
+ })
+
+ it('should warn when using Response.redirect with a relative URL', async () => {
+ const response = await fetchViaHTTP(next.url, `/url/relative-redirect`)
+ expect(readMiddlewareError(response)).toContain(urlsError)
+ })
+ }
+
+ it('should warn when using NextResponse.redirect with a relative URL', async () => {
+ const response = await fetchViaHTTP(next.url, `/url/relative-next-redirect`)
+ expect(readMiddlewareError(response)).toContain(urlsError)
+ })
+
+ it('should throw when using NextResponse.rewrite with a relative URL', async () => {
+ const response = await fetchViaHTTP(next.url, `/url/relative-next-rewrite`)
+ expect(readMiddlewareError(response)).toContain(urlsError)
+ })
+
+ it('should trigger middleware for data requests', async () => {
+ const browser = await webdriver(next.url, `/ssr-page`)
+ const text = await browser.elementByCss('h1').text()
+ expect(text).toEqual('Bye Cruel World')
+ const res = await fetchViaHTTP(next.url, `/_next/data/${next.buildId}${i18n ? '/en' : ''}/ssr-page.json`)
+ const json = await res.json()
+ expect(json.pageProps.message).toEqual('Bye Cruel World')
+ })
+
+ it('should normalize data requests into page requests', async () => {
+ const res = await fetchViaHTTP(next.url, `/_next/data/${next.buildId}${i18n ? '/en' : ''}/send-url.json`)
+ expect(res.headers.get('req-url-path')).toEqual('/send-url')
+ })
+
+ it('should keep non data requests in their original shape', async () => {
+ const res = await fetchViaHTTP(next.url, `/_next/static/${next.buildId}/_devMiddlewareManifest.json?foo=1`)
+ expect(res.headers.get('req-url-path')).toEqual(`/_next/static/${next.buildId}/_devMiddlewareManifest.json?foo=1`)
+ })
+
+ it('should add a rewrite header on data requests for rewrites', async () => {
+ const res = await fetchViaHTTP(next.url, `/ssr-page`)
+ const dataRes = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}${i18n ? '/en' : ''}/ssr-page.json`,
+ undefined,
+ { headers: { 'x-nextjs-data': '1' } },
+ )
+ const json = await dataRes.json()
+ expect(json.pageProps.message).toEqual('Bye Cruel World')
+ expect(res.headers.get('x-nextjs-matched-path')).toBeNull()
+ expect(dataRes.headers.get('x-nextjs-matched-path')).toEqual(`${i18n ? '/en' : ''}/ssr-page-2`)
+ })
+
+ it(`hard-navigates when the data request failed`, async () => {
+ const browser = await webdriver(next.url, `/error`)
+ await browser.eval('window.__SAME_PAGE = true')
+ await browser.elementByCss('#throw-on-data').click()
+ await browser.waitForElementByCss('.refreshed')
+ expect(await browser.eval('window.__SAME_PAGE')).toBeUndefined()
+ })
+
+ it('allows shallow linking with middleware', async () => {
+ const browser = await webdriver(next.url, '/sha')
+ const getMessageContents = () => browser.elementById('message-contents').text()
+ const ssrMessage = await getMessageContents()
+ const requests: string[] = []
+
+ browser.on('request', (x) => {
+ requests.push(x.url())
+ })
+
+ browser.elementById('deep-link').click()
+ browser.waitForElementByCss('[data-query-hello="goodbye"]')
+ const deepLinkMessage = await getMessageContents()
+ expect(deepLinkMessage).not.toEqual(ssrMessage)
+
+ // Changing the route with a shallow link should not cause a server request
+ browser.elementById('shallow-link').click()
+ browser.waitForElementByCss('[data-query-hello="world"]')
+ expect(await getMessageContents()).toEqual(deepLinkMessage)
+
+ // Check that no server requests were made to ?hello=world,
+ // as it's a shallow request.
+ expect(requests).toEqual([`${next.url}/_next/data/${next.buildId}${i18n ? '/en' : ''}/sha.json?hello=goodbye`])
+ })
+ }
+ describe('with i18n', () => {
+ setup({ i18n: true })
+ runTests({ i18n: true })
+ })
+
+ describe('without i18n', () => {
+ setup({ i18n: false })
+ runTests({ i18n: false })
+ })
+})
diff --git a/test/e2e/modified-tests/middleware-redirects/app/middleware.js b/test/e2e/modified-tests/middleware-redirects/app/middleware.js
new file mode 100644
index 0000000000..392e8f98e7
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-redirects/app/middleware.js
@@ -0,0 +1,83 @@
+import { NextResponse } from 'next/server'
+
+export async function middleware(request) {
+ const url = request.nextUrl
+
+ // this is needed for tests to get the BUILD_ID
+ if (url.pathname.startsWith('/_next/static/__BUILD_ID')) {
+ return NextResponse.next()
+ }
+
+ if (url.pathname === '/old-home') {
+ if (url.searchParams.get('override') === 'external') {
+ return Response.redirect('https://example.vercel.sh')
+ } else {
+ url.pathname = '/new-home'
+ return Response.redirect(url)
+ }
+ }
+
+ if (url.searchParams.get('foo') === 'bar') {
+ url.pathname = '/new-home'
+ url.searchParams.delete('foo')
+ return Response.redirect(url)
+ }
+
+ // Chained redirects
+ if (url.pathname === '/redirect-me-alot') {
+ url.pathname = '/redirect-me-alot-2'
+ return Response.redirect(url)
+ }
+
+ if (url.pathname === '/redirect-me-alot-2') {
+ url.pathname = '/redirect-me-alot-3'
+ return Response.redirect(url)
+ }
+
+ if (url.pathname === '/redirect-me-alot-3') {
+ url.pathname = '/redirect-me-alot-4'
+ return Response.redirect(url)
+ }
+
+ if (url.pathname === '/redirect-me-alot-4') {
+ url.pathname = '/redirect-me-alot-5'
+ return Response.redirect(url)
+ }
+
+ if (url.pathname === '/redirect-me-alot-5') {
+ url.pathname = '/redirect-me-alot-6'
+ return Response.redirect(url)
+ }
+
+ if (url.pathname === '/redirect-me-alot-6') {
+ url.pathname = '/redirect-me-alot-7'
+ return Response.redirect(url)
+ }
+
+ if (url.pathname === '/redirect-me-alot-7') {
+ url.pathname = '/new-home'
+ return Response.redirect(url)
+ }
+
+ // Infinite loop
+ if (url.pathname === '/infinite-loop') {
+ url.pathname = '/infinite-loop-1'
+ return Response.redirect(url)
+ }
+
+ if (url.pathname === '/infinite-loop-1') {
+ url.pathname = '/infinite-loop'
+ return Response.redirect(url)
+ }
+
+ if (url.pathname === '/to') {
+ url.pathname = url.searchParams.get('pathname')
+ url.searchParams.delete('pathname')
+ return Response.redirect(url)
+ }
+
+ if (url.pathname === '/with-fragment') {
+ console.log(String(new URL('/new-home#fragment', url)))
+ return Response.redirect(new URL('/new-home#fragment', url))
+ }
+}
diff --git a/test/e2e/modified-tests/middleware-redirects/app/next.config.js b/test/e2e/modified-tests/middleware-redirects/app/next.config.js
new file mode 100644
index 0000000000..4cc6a5bfab
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-redirects/app/next.config.js
@@ -0,0 +1,15 @@
+module.exports = {
+ i18n: {
+ locales: ['en', 'fr', 'nl', 'es'],
+ defaultLocale: 'en',
+ },
+ redirects() {
+ return [
+ {
+ source: '/to-new',
+ destination: '/dynamic/new',
+ permanent: false,
+ },
+ ]
+ },
+}
diff --git a/test/e2e/modified-tests/middleware-redirects/app/pages/_app.js b/test/e2e/modified-tests/middleware-redirects/app/pages/_app.js
new file mode 100644
index 0000000000..65e905445e
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-redirects/app/pages/_app.js
@@ -0,0 +1,8 @@
+export default function App({ Component, pageProps }) {
+ if (!pageProps || typeof pageProps !== 'object') {
+ throw new Error(
+ `Invariant: received invalid pageProps in _app, received ${pageProps}`
+ )
+ }
+ return
+}
diff --git a/test/e2e/modified-tests/middleware-redirects/app/pages/api/ok.js b/test/e2e/modified-tests/middleware-redirects/app/pages/api/ok.js
new file mode 100644
index 0000000000..fb91e8b611
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-redirects/app/pages/api/ok.js
@@ -0,0 +1,3 @@
+export default function handler(req, res) {
+ res.send('ok')
+}
diff --git a/test/e2e/modified-tests/middleware-redirects/app/pages/dynamic/[slug].js b/test/e2e/modified-tests/middleware-redirects/app/pages/dynamic/[slug].js
new file mode 100644
index 0000000000..24df8674ee
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-redirects/app/pages/dynamic/[slug].js
@@ -0,0 +1,13 @@
+export default function Account() {
+ return (
+
+ Welcome to a /dynamic/[slug]
+
+ )
+}
+
+export function getServerSideProps() {
+ return {
+ props: {},
+ }
+}
diff --git a/test/e2e/modified-tests/middleware-redirects/app/pages/index.js b/test/e2e/modified-tests/middleware-redirects/app/pages/index.js
new file mode 100644
index 0000000000..0ca12beb09
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-redirects/app/pages/index.js
@@ -0,0 +1,43 @@
+import Link from 'next/link'
+
+export default function Home() {
+ return (
+
+
Home Page
+
+ Redirect me to a new version of a page
+
+
+
+ Redirect me to an external site
+
+
+
Redirect me with URL params intact
+
+
+ Redirect me to Google (with no body response)
+
+
+
+ Redirect me to Google (with no stream response)
+
+
+
Redirect me alot (chained requests)
+
+
Redirect me alot (infinite loop)
+
+
+ Redirect me to api with locale
+
+
+
+ Redirect me to a redirecting page of new version of page
+
+
+
+ )
+}
diff --git a/test/e2e/modified-tests/middleware-redirects/app/pages/new-home.js b/test/e2e/modified-tests/middleware-redirects/app/pages/new-home.js
new file mode 100644
index 0000000000..313011766e
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-redirects/app/pages/new-home.js
@@ -0,0 +1,7 @@
+export default function Account() {
+ return (
+
+ Welcome to a new page
+
+ )
+}
diff --git a/test/e2e/modified-tests/middleware-redirects/test/index.test.ts b/test/e2e/modified-tests/middleware-redirects/test/index.test.ts
new file mode 100644
index 0000000000..f697ca4973
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-redirects/test/index.test.ts
@@ -0,0 +1,159 @@
+/* eslint-env jest */
+
+import { join } from 'path'
+import cheerio from 'cheerio'
+import webdriver from 'next-webdriver'
+import { check, fetchViaHTTP } from 'next-test-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { createNext, FileRef } from 'e2e-utils'
+const usuallySkip = process.env.RUN_SKIPPED_TESTS ? it : it.skip
+
+describe('Middleware Redirect', () => {
+ let next: NextInstance
+
+ afterAll(() => next.destroy())
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ pages: new FileRef(join(__dirname, '../app/pages')),
+ 'middleware.js': new FileRef(join(__dirname, '../app/middleware.js')),
+ 'next.config.js': new FileRef(join(__dirname, '../app/next.config.js')),
+ },
+ })
+ })
+ function tests() {
+ usuallySkip('should redirect correctly with redirect in next.config.js', async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.eval('window.beforeNav = 1')
+ await browser.eval('window.next.router.push("/to-new")')
+ await browser.waitForElementByCss('#dynamic')
+ expect(await browser.eval('window.beforeNav')).toBe(1)
+ })
+
+ it('does not include the locale in redirects by default', async () => {
+ const res = await fetchViaHTTP(next.url, `/old-home`, undefined, {
+ redirect: 'manual',
+ })
+ expect(res.headers.get('location')?.endsWith('/default/about')).toEqual(false)
+ })
+
+ usuallySkip(`should redirect to data urls with data requests and internal redirects`, async () => {
+ const res = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/es/old-home.json`,
+ { override: 'internal' },
+ { redirect: 'manual', headers: { 'x-nextjs-data': '1' } },
+ )
+
+ expect(res.headers.get('x-nextjs-redirect')?.endsWith(`/es/new-home?override=internal`)).toEqual(true)
+ expect(res.headers.get('location')).toEqual(null)
+ })
+
+ it(`should redirect to external urls with data requests and external redirects`, async () => {
+ const res = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/es/old-home.json`,
+ { override: 'external' },
+ { redirect: 'manual', headers: { 'x-nextjs-data': '1' } },
+ )
+
+ expect(res.headers.get('x-nextjs-redirect')).toEqual('https://example.vercel.sh/')
+ expect(res.headers.get('location')).toEqual(null)
+
+ const browser = await webdriver(next.url, '/')
+ await browser.elementByCss('#old-home-external').click()
+ await check(async () => {
+ expect(await browser.elementByCss('h1').text()).toEqual('Example Domain')
+ return 'yes'
+ }, 'yes')
+ })
+ }
+
+ function testsWithLocale(locale = '') {
+ const label = locale ? `${locale} ` : ``
+
+ it(`${label}should redirect`, async () => {
+ const res = await fetchViaHTTP(next.url, `${locale}/old-home`)
+ const html = await res.text()
+ const $ = cheerio.load(html)
+ const browser = await webdriver(next.url, `${locale}/old-home`)
+ try {
+ expect(await browser.eval(`window.location.pathname`)).toBe(`${locale}/new-home`)
+ } finally {
+ await browser.close()
+ }
+ expect($('.title').text()).toBe('Welcome to a new page')
+ })
+
+ it(`${label}should implement internal redirects`, async () => {
+ const browser = await webdriver(next.url, `${locale}`)
+ await browser.eval('window.__SAME_PAGE = true')
+ await browser.elementByCss('#old-home').click()
+ await browser.waitForElementByCss('#new-home-title')
+ expect(await browser.eval('window.__SAME_PAGE')).toBe(true)
+ try {
+ expect(await browser.eval(`window.location.pathname`)).toBe(`${locale}/new-home`)
+ } finally {
+ await browser.close()
+ }
+ })
+
+ it(`${label}should redirect cleanly with the original url param`, async () => {
+ const browser = await webdriver(next.url, `${locale}/blank-page?foo=bar`)
+ try {
+ expect(await browser.eval(`window.location.href.replace(window.location.origin, '')`)).toBe(
+ `${locale}/new-home`,
+ )
+ } finally {
+ await browser.close()
+ }
+ })
+
+ it(`${label}should redirect multiple times`, async () => {
+ const res = await fetchViaHTTP(next.url, `${locale}/redirect-me-alot`)
+ const browser = await webdriver(next.url, `${locale}/redirect-me-alot`)
+ try {
+ expect(await browser.eval(`window.location.pathname`)).toBe(`${locale}/new-home`)
+ } finally {
+ await browser.close()
+ }
+ const html = await res.text()
+ const $ = cheerio.load(html)
+ expect($('.title').text()).toBe('Welcome to a new page')
+ })
+
+ it(`${label}should redirect (infinite-loop)`, async () => {
+ await expect(fetchViaHTTP(next.url, `${locale}/infinite-loop`)).rejects.toThrow()
+ })
+
+ it(`${label}should redirect to api route with locale`, async () => {
+ const browser = await webdriver(next.url, `${locale}`)
+ await browser.elementByCss('#link-to-api-with-locale').click()
+ await browser.waitForCondition('window.location.pathname === "/api/ok"')
+ await check(() => browser.elementByCss('body').text(), 'ok')
+ const logs = await browser.log()
+ const errors = logs
+ .filter((x) => x.source === 'error')
+ .map((x) => x.message)
+ .join('\n')
+ expect(errors).not.toContain('Failed to lookup route')
+ })
+
+ // A regression test for https://github.com/vercel/next.js/pull/41501
+ it(`${label}should redirect with a fragment`, async () => {
+ const res = await fetchViaHTTP(next.url, `${locale}/with-fragment`)
+ const html = await res.text()
+ const $ = cheerio.load(html)
+ const browser = await webdriver(next.url, `${locale}/with-fragment`)
+ try {
+ expect(await browser.eval(`window.location.hash`)).toBe(`#fragment`)
+ } finally {
+ await browser.close()
+ }
+ expect($('.title').text()).toBe('Welcome to a new page')
+ })
+ }
+ tests()
+ testsWithLocale()
+ testsWithLocale('/fr')
+})
diff --git a/test/e2e/modified-tests/middleware-rewrites/app/middleware.js b/test/e2e/modified-tests/middleware-rewrites/app/middleware.js
new file mode 100644
index 0000000000..d8c89b3292
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-rewrites/app/middleware.js
@@ -0,0 +1,135 @@
+import { NextResponse } from 'next/server'
+
+const PUBLIC_FILE = /\.(.*)$/
+
+/**
+ * @param {import('next/server').NextRequest} request
+ */
+export async function middleware(request) {
+ const url = request.nextUrl
+
+ // this is needed for tests to get the BUILD_ID
+ if (url.pathname.startsWith('/_next/static/__BUILD_ID')) {
+ return NextResponse.next()
+ }
+
+ if (url.pathname.includes('/to/some/404/path')) {
+ return NextResponse.next({
+ 'x-matched-path': '/404',
+ })
+ }
+
+ if (url.pathname.includes('/fallback-true-blog/rewritten')) {
+ request.nextUrl.pathname = '/about'
+ return NextResponse.rewrite(request.nextUrl)
+ }
+
+ if (url.pathname.startsWith('/about') && url.searchParams.has('override')) {
+ const isExternal = url.searchParams.get('override') === 'external'
+ return NextResponse.rewrite(
+ isExternal
+ ? 'https://example.vercel.sh'
+ : new URL('/ab-test/a', request.url)
+ )
+ }
+
+ if (url.pathname === '/rewrite-to-beforefiles-rewrite') {
+ url.pathname = '/beforefiles-rewrite'
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname === '/rewrite-to-afterfiles-rewrite') {
+ url.pathname = '/afterfiles-rewrite'
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname.startsWith('/to-blog')) {
+ const slug = url.pathname.split('/').pop()
+ url.pathname = `/fallback-true-blog/${slug}`
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname === '/rewrite-to-ab-test') {
+ let bucket = request.cookies.get('bucket')
+ if (!bucket) {
+ bucket = Math.random() >= 0.5 ? 'a' : 'b'
+ url.pathname = `/ab-test/${bucket}`
+ const response = NextResponse.rewrite(url)
+ response.cookies.set('bucket', bucket, { maxAge: 10 })
+ return response
+ }
+
+ url.pathname = `/${bucket}`
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname === '/rewrite-me-to-about') {
+ url.pathname = '/about'
+ return NextResponse.rewrite(url, {
+ headers: { 'x-rewrite-target': String(url) },
+ })
+ }
+
+ if (url.pathname === '/rewrite-me-with-a-colon') {
+ url.pathname = '/with:colon'
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname === '/colon:here') {
+ url.pathname = '/no-colon-here'
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname === '/rewrite-me-to-vercel') {
+ return NextResponse.rewrite('https://example.vercel.sh')
+ }
+
+ if (url.pathname === '/clear-query-params') {
+ const allowedKeys = ['allowed']
+ for (const key of [...url.searchParams.keys()]) {
+ if (!allowedKeys.includes(key)) {
+ url.searchParams.delete(key)
+ }
+ }
+ return NextResponse.rewrite(url)
+ }
+
+ if (
+ url.pathname === '/rewrite-me-without-hard-navigation' ||
+ url.searchParams.get('path') === 'rewrite-me-without-hard-navigation'
+ ) {
+ url.searchParams.set('middleware', 'foo')
+ url.pathname = request.cookies.has('about-bypass')
+ ? '/about-bypass'
+ : '/about'
+
+ return NextResponse.rewrite(url, {
+ headers: { 'x-middleware-cache': 'no-cache' },
+ })
+ }
+
+ if (url.pathname.endsWith('/dynamic-replace')) {
+ url.pathname = '/dynamic-fallback/catch-all'
+ return NextResponse.rewrite(url)
+ }
+
+ if (url.pathname.startsWith('/country')) {
+ const locale = url.searchParams.get('my-locale')
+ if (locale) {
+ url.locale = locale
+ }
+
+ const country = url.searchParams.get('country') || 'us'
+ if (!PUBLIC_FILE.test(url.pathname) && !url.pathname.includes('/api/')) {
+ url.pathname = `/country/${country}`
+ return NextResponse.rewrite(url)
+ }
+ }
+
+ if (url.pathname.startsWith('/i18n')) {
+ url.searchParams.set('locale', url.locale)
+ return NextResponse.rewrite(url)
+ }
+
+ return NextResponse.rewrite(request.nextUrl)
+}
diff --git a/test/e2e/modified-tests/middleware-rewrites/app/next.config.js b/test/e2e/modified-tests/middleware-rewrites/app/next.config.js
new file mode 100644
index 0000000000..32c5d38016
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-rewrites/app/next.config.js
@@ -0,0 +1,27 @@
+module.exports = {
+ i18n: {
+ locales: ['ja', 'en', 'fr', 'es'],
+ defaultLocale: 'en',
+ },
+ rewrites() {
+ return {
+ beforeFiles: [
+ {
+ source: '/beforefiles-rewrite',
+ destination: '/ab-test/a',
+ },
+ ],
+ afterFiles: [
+ {
+ source: '/afterfiles-rewrite',
+ destination: '/ab-test/b',
+ },
+ {
+ source: '/afterfiles-rewrite-ssg',
+ destination: '/fallback-true-blog/first',
+ },
+ ],
+ fallback: [],
+ }
+ },
+}
diff --git a/test/e2e/modified-tests/middleware-rewrites/app/pages/404.js b/test/e2e/modified-tests/middleware-rewrites/app/pages/404.js
new file mode 100644
index 0000000000..cc3910106c
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-rewrites/app/pages/404.js
@@ -0,0 +1,3 @@
+export default function NotFound() {
+ return custom 404 page
+}
diff --git a/test/e2e/modified-tests/middleware-rewrites/app/pages/[param].js b/test/e2e/modified-tests/middleware-rewrites/app/pages/[param].js
new file mode 100644
index 0000000000..d33f75dd2d
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-rewrites/app/pages/[param].js
@@ -0,0 +1,12 @@
+export const getServerSideProps = ({ params, query }) => {
+ return { props: { params, query } }
+}
+
+export default function Page({ params: { param }, query: { qp } }) {
+ return (
+ <>
+ {param}
+ {qp}
+ >
+ )
+}
diff --git a/test/e2e/modified-tests/middleware-rewrites/app/pages/_app.js b/test/e2e/modified-tests/middleware-rewrites/app/pages/_app.js
new file mode 100644
index 0000000000..65e905445e
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-rewrites/app/pages/_app.js
@@ -0,0 +1,8 @@
+export default function App({ Component, pageProps }) {
+ if (!pageProps || typeof pageProps !== 'object') {
+ throw new Error(
+ `Invariant: received invalid pageProps in _app, received ${pageProps}`
+ )
+ }
+ return
+}
diff --git a/test/e2e/modified-tests/middleware-rewrites/app/pages/ab-test/a.js b/test/e2e/modified-tests/middleware-rewrites/app/pages/ab-test/a.js
new file mode 100644
index 0000000000..3497b535df
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-rewrites/app/pages/ab-test/a.js
@@ -0,0 +1,9 @@
+export default function Home() {
+ return Welcome Page A
+}
+
+export const getServerSideProps = () => ({
+ props: {
+ abtest: true,
+ },
+})
diff --git a/test/e2e/modified-tests/middleware-rewrites/app/pages/ab-test/b.js b/test/e2e/modified-tests/middleware-rewrites/app/pages/ab-test/b.js
new file mode 100644
index 0000000000..4f15e239e9
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-rewrites/app/pages/ab-test/b.js
@@ -0,0 +1,9 @@
+export default function Home() {
+ return Welcome Page B
+}
+
+export const getServerSideProps = () => ({
+ props: {
+ abtest: true,
+ },
+})
diff --git a/test/e2e/modified-tests/middleware-rewrites/app/pages/about-bypass.js b/test/e2e/modified-tests/middleware-rewrites/app/pages/about-bypass.js
new file mode 100644
index 0000000000..4039fc76ad
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-rewrites/app/pages/about-bypass.js
@@ -0,0 +1,12 @@
+export default function AboutBypass({ message }) {
+ return (
+
+
About Bypassed Page
+
{message}
+
+ )
+}
+
+export const getServerSideProps = ({ query }) => ({
+ props: { message: query.message || '' },
+})
diff --git a/test/e2e/modified-tests/middleware-rewrites/app/pages/about.js b/test/e2e/modified-tests/middleware-rewrites/app/pages/about.js
new file mode 100644
index 0000000000..4eff796b31
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-rewrites/app/pages/about.js
@@ -0,0 +1,16 @@
+export default function Main({ message, middleware }) {
+ return (
+
+
About Page
+
{message}
+
{middleware}
+
+ )
+}
+
+export const getServerSideProps = ({ query }) => ({
+ props: {
+ middleware: query.middleware || '',
+ message: query.message || '',
+ },
+})
diff --git a/test/e2e/modified-tests/middleware-rewrites/app/pages/clear-query-params.js b/test/e2e/modified-tests/middleware-rewrites/app/pages/clear-query-params.js
new file mode 100644
index 0000000000..2901a155e8
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-rewrites/app/pages/clear-query-params.js
@@ -0,0 +1,12 @@
+export default function ClearQueryParams(props) {
+ return {JSON.stringify(props.query)}
+}
+
+/** @type {import('next').GetServerSideProps} */
+export const getServerSideProps = (req) => {
+ return {
+ props: {
+ query: { ...req.query },
+ },
+ }
+}
diff --git a/test/e2e/modified-tests/middleware-rewrites/app/pages/country/[country].js b/test/e2e/modified-tests/middleware-rewrites/app/pages/country/[country].js
new file mode 100644
index 0000000000..dd09ad84a7
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-rewrites/app/pages/country/[country].js
@@ -0,0 +1,22 @@
+export const getStaticPaths = async () => {
+ return {
+ fallback: 'blocking',
+ paths: [],
+ }
+}
+
+export const getStaticProps = async ({ params: { country }, locale }) => {
+ return {
+ props: { country, locale },
+ revalidate: false,
+ }
+}
+
+export default function CountryPage({ locale, country }) {
+ return (
+
+ )
+}
diff --git a/test/e2e/modified-tests/middleware-rewrites/app/pages/dynamic-fallback/[...parts].js b/test/e2e/modified-tests/middleware-rewrites/app/pages/dynamic-fallback/[...parts].js
new file mode 100644
index 0000000000..7f91f8690f
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-rewrites/app/pages/dynamic-fallback/[...parts].js
@@ -0,0 +1,5 @@
+const PartsPage = () => {
+ return Parts page
+}
+
+export default PartsPage
diff --git a/test/e2e/modified-tests/middleware-rewrites/app/pages/fallback-true-blog/[slug].js b/test/e2e/modified-tests/middleware-rewrites/app/pages/fallback-true-blog/[slug].js
new file mode 100644
index 0000000000..a2aa1488ce
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-rewrites/app/pages/fallback-true-blog/[slug].js
@@ -0,0 +1,48 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+export default function Page(props) {
+ if (useRouter().isFallback) {
+ return Loading...
+ }
+
+ return (
+ <>
+ {JSON.stringify(props)}
+
+ to /fallback-true-blog/first?hello=world
+
+
+
+ to /fallback-true-blog/second
+
+
+ >
+ )
+}
+
+export function getStaticPaths() {
+ return {
+ paths: [
+ '/fallback-true-blog/first',
+ '/fallback-true-blog/build-time-1',
+ '/fallback-true-blog/build-time-2',
+ '/fallback-true-blog/build-time-3',
+ '/fallback-true-blog/build-time-4',
+ ],
+ fallback: true,
+ }
+}
+
+export function getStaticProps({ params }) {
+ return {
+ props: {
+ params,
+ time: Date.now(),
+ },
+ }
+}
diff --git a/test/e2e/modified-tests/middleware-rewrites/app/pages/i18n.js b/test/e2e/modified-tests/middleware-rewrites/app/pages/i18n.js
new file mode 100644
index 0000000000..0c3d2b993d
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-rewrites/app/pages/i18n.js
@@ -0,0 +1,40 @@
+import Link from 'next/link'
+
+export default function Home({ locale }) {
+ return (
+
+
{locale}
+
+
+
+ Go to en
+
+
+
+
+ Go to en2
+
+
+
+
+ Go to ja
+
+
+
+
+ Go to ja2
+
+
+
+
+ Go to fr
+
+
+
+
+ )
+}
+
+export const getServerSideProps = ({ query }) => ({
+ props: { locale: query.locale },
+})
diff --git a/test/e2e/modified-tests/middleware-rewrites/app/pages/index.js b/test/e2e/modified-tests/middleware-rewrites/app/pages/index.js
new file mode 100644
index 0000000000..817ddf4077
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-rewrites/app/pages/index.js
@@ -0,0 +1,82 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+export default function Home() {
+ const router = useRouter()
+ return (
+
+ )
+}
+
+export function getServerSideProps() {
+ return {
+ props: {
+ now: Date.now(),
+ },
+ }
+}
diff --git a/test/e2e/modified-tests/middleware-rewrites/app/pages/ssg.js b/test/e2e/modified-tests/middleware-rewrites/app/pages/ssg.js
new file mode 100644
index 0000000000..7bdaed70bf
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-rewrites/app/pages/ssg.js
@@ -0,0 +1,14 @@
+export default function Main({ now }) {
+ return (
+
+ )
+}
+
+export const getStaticProps = () => ({
+ props: {
+ now: Date.now(),
+ },
+})
diff --git a/test/e2e/modified-tests/middleware-rewrites/test/index.test.ts b/test/e2e/modified-tests/middleware-rewrites/test/index.test.ts
new file mode 100644
index 0000000000..0095a383e1
--- /dev/null
+++ b/test/e2e/modified-tests/middleware-rewrites/test/index.test.ts
@@ -0,0 +1,619 @@
+/* eslint-env jest */
+
+import { join } from 'path'
+import cheerio from 'cheerio'
+import webdriver from 'next-webdriver'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { check, fetchViaHTTP } from 'next-test-utils'
+import { createNext, FileRef } from 'e2e-utils'
+import escapeStringRegexp from 'escape-string-regexp'
+const usuallySkip = process.env.RUN_SKIPPED_TESTS ? it : it.skip
+
+describe('Middleware Rewrite', () => {
+ let next: NextInstance
+
+ afterAll(() => next.destroy())
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ pages: new FileRef(join(__dirname, '../app/pages')),
+ 'next.config.js': new FileRef(join(__dirname, '../app/next.config.js')),
+ 'middleware.js': new FileRef(join(__dirname, '../app/middleware.js')),
+ },
+ })
+ })
+
+ function tests() {
+ it('should not have un-necessary data request on rewrite', async () => {
+ const browser = await webdriver(next.url, '/to-blog/first', {
+ waitHydration: false,
+ })
+ let requests = []
+
+ browser.on('request', (req) => {
+ requests.push(new URL(req.url()).pathname)
+ })
+
+ await check(() => browser.eval(`next.router.isReady ? "yup" : "nope"`), 'yup')
+
+ expect(requests.filter((url) => url === '/fallback-true-blog/first.json').length).toBeLessThan(2)
+ })
+
+ it('should not mix component cache when navigating between dynamic routes', async () => {
+ const browser = await webdriver(next.url, '/param-1')
+
+ expect(await browser.eval('next.router.pathname')).toBe('/[param]')
+ expect(await browser.eval('next.router.query.param')).toBe('param-1')
+
+ await browser.eval(`next.router.push("/fallback-true-blog/first")`)
+ await check(() => browser.eval('next.router.pathname'), '/fallback-true-blog/[slug]')
+ expect(await browser.eval('next.router.query.slug')).toBe('first')
+ expect(await browser.eval('next.router.asPath')).toBe('/fallback-true-blog/first')
+
+ await browser.back()
+ await check(() => browser.eval('next.router.pathname'), '/[param]')
+ expect(await browser.eval('next.router.query.param')).toBe('param-1')
+ expect(await browser.eval('next.router.asPath')).toBe('/param-1')
+ })
+
+ // NTL Fail
+ usuallySkip('should have props for afterFiles rewrite to SSG page', async () => {
+ let browser = await webdriver(next.url, '/')
+ await browser.eval(`next.router.push("/afterfiles-rewrite-ssg")`)
+
+ await check(() => browser.eval('next.router.isReady ? "yup": "nope"'), 'yup')
+ await check(() => browser.eval('document.documentElement.innerHTML'), /"slug":"first"/)
+
+ browser = await webdriver(next.url, '/afterfiles-rewrite-ssg')
+ await check(() => browser.eval('next.router.isReady ? "yup": "nope"'), 'yup')
+ await check(() => browser.eval('document.documentElement.innerHTML'), /"slug":"first"/)
+ })
+
+ it('should hard navigate on 404 for data request', async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.eval('window.beforeNav = 1')
+ await browser.eval(`next.router.push("/to/some/404/path")`)
+ await check(() => browser.eval('document.documentElement.innerHTML'), /custom 404 page/)
+ expect(await browser.eval('location.pathname')).toBe('/to/some/404/path')
+ expect(await browser.eval('window.beforeNav')).not.toBe(1)
+ })
+
+ // TODO: middleware effect headers aren't available here
+ usuallySkip('includes the locale in rewrites by default', async () => {
+ const res = await fetchViaHTTP(next.url, `/rewrite-me-to-about`)
+ expect(res.headers.get('x-middleware-rewrite')?.endsWith('/en/about')).toEqual(true)
+ })
+
+ it('should rewrite correctly when navigating via history', async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.elementByCss('#override-with-internal-rewrite').click()
+ await check(() => {
+ return browser.eval('document.documentElement.innerHTML')
+ }, /Welcome Page A/)
+
+ await browser.refresh()
+ await browser.back()
+ await browser.waitForElementByCss('#override-with-internal-rewrite')
+ await browser.forward()
+ await check(() => {
+ return browser.eval('document.documentElement.innerHTML')
+ }, /Welcome Page A/)
+ })
+
+ it('should rewrite correctly when navigating via history after query update', async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.elementByCss('#override-with-internal-rewrite').click()
+ await check(() => {
+ return browser.eval('document.documentElement.innerHTML')
+ }, /Welcome Page A/)
+
+ await browser.refresh()
+ await browser.waitForCondition(`!!window.next.router.isReady`)
+ await browser.back()
+ await browser.waitForElementByCss('#override-with-internal-rewrite')
+ await browser.forward()
+ await check(() => {
+ return browser.eval('document.documentElement.innerHTML')
+ }, /Welcome Page A/)
+ })
+
+ it('should return HTML/data correctly for pre-rendered page', async () => {
+ for (const slug of ['first', 'build-time-1', 'build-time-2', 'build-time-3']) {
+ const res = await fetchViaHTTP(next.url, `/fallback-true-blog/${slug}`)
+ expect(res.status).toBe(200)
+
+ const $ = cheerio.load(await res.text())
+ expect(JSON.parse($('#props').text())?.params).toEqual({
+ slug,
+ })
+
+ const dataRes = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/en/fallback-true-blog/${slug}.json`,
+ undefined,
+ {
+ headers: {
+ 'x-nextjs-data': '1',
+ },
+ },
+ )
+ expect(dataRes.status).toBe(200)
+ expect((await dataRes.json())?.pageProps?.params).toEqual({
+ slug,
+ })
+ }
+ })
+
+ it('should override with rewrite internally correctly', async () => {
+ const res = await fetchViaHTTP(next.url, `/about`, { override: 'internal' }, { redirect: 'manual' })
+
+ expect(res.status).toBe(200)
+ expect(await res.text()).toContain('Welcome Page A')
+
+ const browser = await webdriver(next.url, ``)
+ await browser.elementByCss('#override-with-internal-rewrite').click()
+ await check(() => browser.eval('document.documentElement.innerHTML'), /Welcome Page A/)
+ expect(await browser.eval('window.location.pathname')).toBe(`/about`)
+ expect(await browser.eval('window.location.search')).toBe('?override=internal')
+ })
+
+ it(`should rewrite to data urls for incoming data request internally rewritten`, async () => {
+ const res = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/es/about.json`,
+ { override: 'internal' },
+ { redirect: 'manual', headers: { 'x-nextjs-data': '1' } },
+ )
+ const json = await res.json()
+ expect(json.pageProps).toEqual({ abtest: true })
+ })
+
+ it('should override with rewrite externally correctly', async () => {
+ const res = await fetchViaHTTP(next.url, `/about`, { override: 'external' }, { redirect: 'manual' })
+
+ expect(res.status).toBe(200)
+ expect(await res.text()).toContain('Example Domain')
+
+ const browser = await webdriver(next.url, ``)
+ await browser.elementByCss('#override-with-external-rewrite').click()
+ await check(() => browser.eval('document.documentElement.innerHTML'), /Example Domain/)
+ await check(() => browser.eval('window.location.pathname'), `/about`)
+ await check(() => browser.eval('window.location.search'), '?override=external')
+ })
+
+ it(`should rewrite to the external url for incoming data request externally rewritten`, async () => {
+ const browser = await webdriver(
+ next.url,
+ `/_next/data/${next.buildId}/es/about.json?override=external`,
+ undefined,
+ )
+ await check(() => browser.eval('document.documentElement.innerHTML'), /Example Domain/)
+ })
+ // NTL Fail
+ usuallySkip('should rewrite to fallback: true page successfully', async () => {
+ const randomSlug = `another-${Date.now()}`
+ const res2 = await fetchViaHTTP(next.url, `/to-blog/${randomSlug}`)
+ expect(res2.status).toBe(200)
+ expect(await res2.text()).toContain('Loading...')
+
+ const randomSlug2 = `another-${Date.now()}`
+ const browser = await webdriver(next.url, `/to-blog/${randomSlug2}`)
+
+ await check(async () => {
+ const props = JSON.parse(await browser.elementByCss('#props').text())
+ return props.params.slug === randomSlug2 ? 'success' : JSON.stringify(props)
+ }, 'success')
+ })
+
+ it('should allow to opt-out prefetch caching', async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.addCookie({ name: 'about-bypass', value: '1' })
+ await browser.refresh()
+ await browser.eval('window.__SAME_PAGE = true')
+ await browser.elementByCss('#link-with-rewritten-url').click()
+ await browser.waitForElementByCss('.refreshed')
+ await browser.deleteCookies()
+ expect(await browser.eval('window.__SAME_PAGE')).toBe(true)
+ const element = await browser.elementByCss('.title')
+ expect(await element.text()).toEqual('About Bypassed Page')
+ })
+
+ if (!(global as any).isNextDev) {
+ it('should not prefetch non-SSG routes', async () => {
+ const browser = await webdriver(next.url, '/')
+
+ await check(async () => {
+ const hrefs = await browser.eval(`Object.keys(window.next.router.sdc)`)
+ for (const url of ['/en/ssg.json']) {
+ if (!hrefs.some((href) => href.includes(url))) {
+ return JSON.stringify(hrefs, null, 2)
+ }
+ }
+ return 'yes'
+ }, 'yes')
+ })
+ }
+
+ it(`should allow to rewrite keeping the locale in pathname`, async () => {
+ const res = await fetchViaHTTP(next.url, '/fr/country', {
+ country: 'spain',
+ })
+ const html = await res.text()
+ const $ = cheerio.load(html)
+ expect($('#locale').text()).toBe('fr')
+ expect($('#country').text()).toBe('spain')
+ })
+
+ it(`should allow to rewrite to a different locale`, async () => {
+ const res = await fetchViaHTTP(next.url, '/country', {
+ 'my-locale': 'es',
+ })
+ const html = await res.text()
+ const $ = cheerio.load(html)
+ expect($('#locale').text()).toBe('es')
+ expect($('#country').text()).toBe('us')
+ })
+
+ it(`should behave consistently on recursive rewrites`, async () => {
+ const res = await fetchViaHTTP(next.url, `/rewrite-me-to-about`, {
+ override: 'internal',
+ })
+ const html = await res.text()
+ const $ = cheerio.load(html)
+ expect($('.title').text()).toBe('About Page')
+
+ const browser = await webdriver(next.url, `/`)
+ await browser.elementByCss('#rewrite-me-to-about').click()
+ await check(() => browser.eval(`window.location.pathname`), `/rewrite-me-to-about`)
+ const element = await browser.elementByCss('.title')
+ expect(await element.text()).toEqual('About Page')
+ })
+
+ it(`should allow to switch locales`, async () => {
+ const browser = await webdriver(next.url, '/i18n')
+ await browser.waitForElementByCss('.en')
+ await browser.elementByCss('#link-ja').click()
+ await browser.waitForElementByCss('.ja')
+ await browser.elementByCss('#link-en').click()
+ await browser.waitForElementByCss('.en')
+ await browser.elementByCss('#link-fr').click()
+ await browser.waitForElementByCss('.fr')
+ await browser.elementByCss('#link-ja2').click()
+ await browser.waitForElementByCss('.ja')
+ await browser.elementByCss('#link-en2').click()
+ await browser.waitForElementByCss('.en')
+ })
+
+ it('should allow to rewrite to a `beforeFiles` rewrite config', async () => {
+ const res = await fetchViaHTTP(next.url, `/rewrite-to-beforefiles-rewrite`)
+ expect(res.status).toBe(200)
+ expect(await res.text()).toContain('Welcome Page A')
+
+ const browser = await webdriver(next.url, '/')
+ await browser.elementByCss('#rewrite-to-beforefiles-rewrite').click()
+ await check(() => browser.eval('document.documentElement.innerHTML'), /Welcome Page A/)
+ expect(await browser.eval('window.location.pathname')).toBe(`/rewrite-to-beforefiles-rewrite`)
+ })
+
+ it('should allow to rewrite to a `afterFiles` rewrite config', async () => {
+ const res = await fetchViaHTTP(next.url, `/rewrite-to-afterfiles-rewrite`)
+ expect(res.status).toBe(200)
+ expect(await res.text()).toContain('Welcome Page B')
+
+ const browser = await webdriver(next.url, '/')
+ await browser.elementByCss('#rewrite-to-afterfiles-rewrite').click()
+ await check(() => browser.eval('document.documentElement.innerHTML'), /Welcome Page B/)
+ expect(await browser.eval('window.location.pathname')).toBe(`/rewrite-to-afterfiles-rewrite`)
+ })
+
+ it('should have correct query info for dynamic route after query hydration', async () => {
+ const browser = await webdriver(next.url, '/fallback-true-blog/first?hello=world')
+
+ await check(
+ () => browser.eval('next.router.query.hello === "world" ? "success" : JSON.stringify(next.router.query)'),
+ 'success',
+ )
+
+ expect(await browser.eval('next.router.query')).toEqual({
+ slug: 'first',
+ hello: 'world',
+ })
+ expect(await browser.eval('location.pathname')).toBe('/fallback-true-blog/first')
+ expect(await browser.eval('location.search')).toBe('?hello=world')
+ })
+
+ it('should handle shallow navigation correctly (non-dynamic page)', async () => {
+ const browser = await webdriver(next.url, '/about')
+ const requests = []
+
+ browser.on('request', (req) => {
+ const url = req.url()
+ if (url.includes('_next/data')) requests.push(url)
+ })
+
+ await browser.eval(`next.router.push('/about?hello=world', undefined, { shallow: true })`)
+ await check(() => browser.eval(`next.router.query.hello`), 'world')
+
+ expect(await browser.eval(`next.router.pathname`)).toBe('/about')
+ expect(JSON.parse(await browser.eval(`JSON.stringify(next.router.query)`))).toEqual({ hello: 'world' })
+ expect(await browser.eval('location.pathname')).toBe('/about')
+ expect(await browser.eval('location.search')).toBe('?hello=world')
+
+ await browser.eval(`next.router.push('/about', undefined, { shallow: true })`)
+ await check(() => browser.eval(`next.router.query.hello || 'empty'`), 'empty')
+
+ expect(await browser.eval(`next.router.pathname`)).toBe('/about')
+ expect(JSON.parse(await browser.eval(`JSON.stringify(next.router.query)`))).toEqual({})
+ expect(await browser.eval('location.pathname')).toBe('/about')
+ expect(await browser.eval('location.search')).toBe('')
+ })
+
+ it('should handle shallow navigation correctly (dynamic page)', async () => {
+ const browser = await webdriver(next.url, '/fallback-true-blog/first')
+
+ await check(async () => {
+ await browser.elementByCss('#to-query-shallow').click()
+ return browser.eval('location.search')
+ }, '?hello=world')
+
+ expect(await browser.eval(`next.router.pathname`)).toBe('/fallback-true-blog/[slug]')
+ expect(JSON.parse(await browser.eval(`JSON.stringify(next.router.query)`))).toEqual({
+ hello: 'world',
+ slug: 'first',
+ })
+ expect(await browser.eval('location.pathname')).toBe('/fallback-true-blog/first')
+ expect(await browser.eval('location.search')).toBe('?hello=world')
+
+ await browser.elementByCss('#to-no-query-shallow').click()
+ await check(() => browser.eval(`next.router.query.slug`), 'second')
+
+ expect(await browser.eval(`next.router.pathname`)).toBe('/fallback-true-blog/[slug]')
+ expect(JSON.parse(await browser.eval(`JSON.stringify(next.router.query)`))).toEqual({
+ slug: 'second',
+ })
+ expect(await browser.eval('location.pathname')).toBe('/fallback-true-blog/second')
+ expect(await browser.eval('location.search')).toBe('')
+ })
+
+ it('should resolve dynamic route after rewrite correctly', async () => {
+ const browser = await webdriver(next.url, '/fallback-true-blog/first', {
+ waitHydration: false,
+ })
+ let requests = []
+
+ browser.on('request', (req) => {
+ const url = new URL(req.url().replace(new RegExp(escapeStringRegexp(next.buildId)), 'BUILD_ID')).pathname
+
+ if (url.includes('_next/data')) requests.push(url)
+ })
+
+ // wait for initial query update request
+ await check(async () => {
+ const didReq = await browser.eval('next.router.isReady')
+ if (requests.length > 0 || didReq) {
+ requests = []
+ return 'yup'
+ }
+ }, 'yup')
+
+ expect(await browser.eval(`next.router.pathname`)).toBe('/fallback-true-blog/[slug]')
+ expect(JSON.parse(await browser.eval(`JSON.stringify(next.router.query)`))).toEqual({
+ slug: 'first',
+ })
+ expect(await browser.eval('location.pathname')).toBe('/fallback-true-blog/first')
+ expect(await browser.eval('location.search')).toBe('')
+
+ await browser.eval(`next.router.push('/fallback-true-blog/rewritten')`)
+ await check(() => browser.eval('document.documentElement.innerHTML'), /About Page/)
+
+ expect(await browser.eval(`next.router.pathname`)).toBe('/about')
+ expect(JSON.parse(await browser.eval(`JSON.stringify(next.router.query)`))).toEqual({})
+ expect(await browser.eval('location.pathname')).toBe('/fallback-true-blog/rewritten')
+ expect(await browser.eval('location.search')).toBe('')
+ expect(requests.some((req) => req === `/_next/data/BUILD_ID/en/fallback-true-blog/rewritten.json`)).toBe(true)
+
+ await browser.eval(`next.router.push('/fallback-true-blog/second')`)
+ await check(() => browser.eval(`next.router.pathname`), '/fallback-true-blog/[slug]')
+
+ expect(await browser.eval(`next.router.pathname`)).toBe('/fallback-true-blog/[slug]')
+ expect(JSON.parse(await browser.eval(`JSON.stringify(next.router.query)`))).toEqual({
+ slug: 'second',
+ })
+ expect(await browser.eval('location.pathname')).toBe('/fallback-true-blog/second')
+ expect(await browser.eval('location.search')).toBe('')
+ })
+ }
+
+ function testsWithLocale(locale = '') {
+ const label = locale ? `${locale} ` : ``
+
+ function getCookieFromResponse(res, cookieName) {
+ // node-fetch bundles the cookies as string in the Response
+ const cookieArray = res.headers.raw()['set-cookie']
+ for (const cookie of cookieArray) {
+ let individualCookieParams = cookie.split(';')
+ let individualCookie = individualCookieParams[0].split('=')
+ if (individualCookie[0] === cookieName) {
+ return individualCookie[1]
+ }
+ }
+ return -1
+ }
+
+ it(`${label}should add a cookie and rewrite to a/b test`, async () => {
+ const res = await fetchViaHTTP(next.url, `${locale}/rewrite-to-ab-test`)
+ const html = await res.text()
+ const $ = cheerio.load(html)
+ // Set-Cookie header with Expires should not be split into two
+ expect(res.headers.raw()['set-cookie']).toHaveLength(1)
+ const bucket = getCookieFromResponse(res, 'bucket')
+ const expectedText = bucket === 'a' ? 'Welcome Page A' : 'Welcome Page B'
+ const browser = await webdriver(next.url, `${locale}/rewrite-to-ab-test`)
+ try {
+ expect(await browser.eval(`window.location.pathname`)).toBe(`${locale}/rewrite-to-ab-test`)
+ } finally {
+ await browser.close()
+ }
+ // -1 is returned if bucket was not found in func getCookieFromResponse
+ expect(bucket).not.toBe(-1)
+ expect($('.title').text()).toBe(expectedText)
+ })
+
+ it(`${label}should clear query parameters`, async () => {
+ const res = await fetchViaHTTP(next.url, `${locale}/clear-query-params`, {
+ a: '1',
+ b: '2',
+ foo: 'bar',
+ allowed: 'kept',
+ })
+ const html = await res.text()
+ const $ = cheerio.load(html)
+ expect(JSON.parse($('#my-query-params').text())).toEqual({
+ allowed: 'kept',
+ })
+ })
+
+ it(`${label}should rewrite to about page`, async () => {
+ const res = await fetchViaHTTP(next.url, `${locale}/rewrite-me-to-about`)
+ const html = await res.text()
+ const $ = cheerio.load(html)
+ const browser = await webdriver(next.url, `${locale}/rewrite-me-to-about`)
+ try {
+ expect(await browser.eval(`window.location.pathname`)).toBe(`${locale}/rewrite-me-to-about`)
+ } finally {
+ await browser.close()
+ }
+ expect($('.title').text()).toBe('About Page')
+ })
+
+ it(`${label}support colons in path`, async () => {
+ const path = `${locale}/not:param`
+ const res = await fetchViaHTTP(next.url, path)
+ const html = await res.text()
+ const $ = cheerio.load(html)
+ expect($('#props').text()).toBe('not:param')
+ const browser = await webdriver(next.url, path)
+ try {
+ expect(await browser.eval(`window.location.pathname`)).toBe(path)
+ } finally {
+ await browser.close()
+ }
+ })
+
+ it(`${label}can rewrite to path with colon`, async () => {
+ const path = `${locale}/rewrite-me-with-a-colon`
+ const res = await fetchViaHTTP(next.url, path)
+ const html = await res.text()
+ const $ = cheerio.load(html)
+ expect($('#props').text()).toBe('with:colon')
+ const browser = await webdriver(next.url, path)
+ try {
+ expect(await browser.eval(`window.location.pathname`)).toBe(path)
+ } finally {
+ await browser.close()
+ }
+ })
+
+ it(`${label}can rewrite from path with colon`, async () => {
+ const path = `${locale}/colon:here`
+ const res = await fetchViaHTTP(next.url, path)
+ const html = await res.text()
+ const $ = cheerio.load(html)
+ expect($('#props').text()).toBe('no-colon-here')
+ const browser = await webdriver(next.url, path)
+ try {
+ expect(await browser.eval(`window.location.pathname`)).toBe(path)
+ } finally {
+ await browser.close()
+ }
+ })
+
+ it(`${label}can rewrite from path with colon and retain query parameter`, async () => {
+ const path = `${locale}/colon:here?qp=arg`
+ const res = await fetchViaHTTP(next.url, path)
+ const html = await res.text()
+ const $ = cheerio.load(html)
+ expect($('#props').text()).toBe('no-colon-here')
+ expect($('#qp').text()).toBe('arg')
+ const browser = await webdriver(next.url, path)
+ try {
+ expect(await browser.eval(`window.location.href.replace(window.location.origin, '')`)).toBe(path)
+ } finally {
+ await browser.close()
+ }
+ })
+
+ it(`${label}can rewrite to path with colon and retain query parameter`, async () => {
+ const path = `${locale}/rewrite-me-with-a-colon?qp=arg`
+ const res = await fetchViaHTTP(next.url, path)
+ const html = await res.text()
+ const $ = cheerio.load(html)
+ expect($('#props').text()).toBe('with:colon')
+ expect($('#qp').text()).toBe('arg')
+ const browser = await webdriver(next.url, path)
+ try {
+ expect(await browser.eval(`window.location.href.replace(window.location.origin, '')`)).toBe(path)
+ } finally {
+ await browser.close()
+ }
+ })
+
+ if (!(global as any).isNextDeploy) {
+ it(`${label}should rewrite when not using localhost`, async () => {
+ const customUrl = new URL(next.url)
+ customUrl.hostname = 'localtest.me'
+
+ const res = await fetchViaHTTP(customUrl.toString(), `${locale}/rewrite-me-without-hard-navigation`)
+ const html = await res.text()
+ const $ = cheerio.load(html)
+ expect($('.title').text()).toBe('About Page')
+ })
+ }
+
+ it(`${label}should rewrite to Vercel`, async () => {
+ const res = await fetchViaHTTP(next.url, `${locale}/rewrite-me-to-vercel`)
+ const html = await res.text()
+ // const browser = await webdriver(next.url, '/rewrite-me-to-vercel')
+ // TODO: running this to chech the window.location.pathname hangs for some reason;
+ expect(html).toContain('Example Domain')
+ })
+
+ it(`${label}should rewrite without hard navigation`, async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.eval('window.__SAME_PAGE = true')
+ await browser.elementByCss('#link-with-rewritten-url').click()
+ await browser.waitForElementByCss('.refreshed')
+ expect(await browser.eval('window.__SAME_PAGE')).toBe(true)
+ const element = await browser.elementByCss('.middleware')
+ expect(await element.text()).toEqual('foo')
+ })
+
+ it(`${label}should not call middleware with shallow push`, async () => {
+ const browser = await webdriver(next.url, '')
+ await browser.elementByCss('#link-to-shallow-push').click()
+ await browser.waitForCondition(
+ 'new URL(window.location.href).searchParams.get("path") === "rewrite-me-without-hard-navigation"',
+ )
+ await expect(async () => {
+ await browser.waitForElementByCss('.refreshed', 500)
+ }).rejects.toThrow()
+ })
+
+ it(`${label}should correctly rewriting to a different dynamic path`, async () => {
+ const browser = await webdriver(next.url, '/dynamic-replace')
+ const element = await browser.elementByCss('.title')
+ expect(await element.text()).toEqual('Parts page')
+ const logs = await browser.log()
+ expect(logs.every((log) => log.source === 'log' || log.source === 'info')).toEqual(true)
+ })
+
+ it('should not have unexpected errors', async () => {
+ expect(next.cliOutput).not.toContain('unhandledRejection')
+ expect(next.cliOutput).not.toContain('ECONNRESET')
+ })
+ }
+
+ tests()
+ testsWithLocale()
+ testsWithLocale('/fr')
+})
diff --git a/test/e2e/modified-tests/skip-trailing-slash-redirect/app/middleware.js b/test/e2e/modified-tests/skip-trailing-slash-redirect/app/middleware.js
new file mode 100644
index 0000000000..bc28d0ae47
--- /dev/null
+++ b/test/e2e/modified-tests/skip-trailing-slash-redirect/app/middleware.js
@@ -0,0 +1,49 @@
+import { NextResponse } from 'next/server'
+
+export default function handler(req) {
+ console.log(req.nextUrl)
+
+ if (req.nextUrl.pathname.startsWith('/_next/data/missing-id')) {
+ console.log(`missing-id rewrite: ${req.nextUrl.toString()}`)
+ return NextResponse.rewrite('https://example.vercel.sh')
+ }
+
+ if (
+ req.nextUrl.pathname.startsWith('/_next/data') &&
+ req.nextUrl.pathname.endsWith('valid.json')
+ ) {
+ return NextResponse.rewrite('https://example.vercel.sh')
+ }
+
+ if (req.nextUrl.pathname.includes('/middleware-rewrite-with-slash')) {
+ return NextResponse.rewrite(new URL('/another/', req.nextUrl))
+ }
+
+ if (req.nextUrl.pathname.includes('/middleware-rewrite-without-slash')) {
+ return NextResponse.rewrite(new URL('/another', req.nextUrl))
+ }
+
+ if (req.nextUrl.pathname === '/middleware-redirect-external-with') {
+ return NextResponse.redirect('https://example.vercel.sh/somewhere/', 307)
+ }
+
+ if (req.nextUrl.pathname === '/middleware-redirect-external-without') {
+ return NextResponse.redirect('https://example.vercel.sh/somewhere', 307)
+ }
+
+ if (req.nextUrl.pathname.startsWith('/api/test-cookie')) {
+ const res = NextResponse.next()
+ res.cookies.set('from-middleware', 1)
+ return res
+ }
+
+ if (req.nextUrl.pathname === '/middleware-response-body') {
+ return new Response('hello from middleware', {
+ headers: {
+ 'x-from-middleware': 'true',
+ },
+ })
+ }
+
+ return NextResponse.next()
+}
diff --git a/test/e2e/modified-tests/skip-trailing-slash-redirect/app/next.config.js b/test/e2e/modified-tests/skip-trailing-slash-redirect/app/next.config.js
new file mode 100644
index 0000000000..83ae7164fa
--- /dev/null
+++ b/test/e2e/modified-tests/skip-trailing-slash-redirect/app/next.config.js
@@ -0,0 +1,27 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ experimental: {
+ skipTrailingSlashRedirect: true,
+ skipMiddlewareUrlNormalize: true,
+ allowMiddlewareResponseBody: true,
+ },
+ async redirects() {
+ return [
+ {
+ source: '/redirect-me',
+ destination: '/another',
+ permanent: false,
+ },
+ ]
+ },
+ async rewrites() {
+ return [
+ {
+ source: '/rewrite-me',
+ destination: '/another',
+ },
+ ]
+ },
+}
+
+module.exports = nextConfig
diff --git a/test/e2e/modified-tests/skip-trailing-slash-redirect/app/pages/another.js b/test/e2e/modified-tests/skip-trailing-slash-redirect/app/pages/another.js
new file mode 100644
index 0000000000..b6cb822ba1
--- /dev/null
+++ b/test/e2e/modified-tests/skip-trailing-slash-redirect/app/pages/another.js
@@ -0,0 +1,13 @@
+import Link from 'next/link'
+
+export default function Page(props) {
+ return (
+ <>
+ another page
+
+ to index
+
+
+ >
+ )
+}
diff --git a/test/e2e/modified-tests/skip-trailing-slash-redirect/app/pages/api/test-cookie-edge.js b/test/e2e/modified-tests/skip-trailing-slash-redirect/app/pages/api/test-cookie-edge.js
new file mode 100644
index 0000000000..6018a22370
--- /dev/null
+++ b/test/e2e/modified-tests/skip-trailing-slash-redirect/app/pages/api/test-cookie-edge.js
@@ -0,0 +1,12 @@
+import { NextResponse } from 'next/server'
+
+export const config = {
+ runtime: 'experimental-edge',
+}
+
+export default function handler(req) {
+ console.log('setting cookie in api route')
+ const res = NextResponse.json({ name: 'API' })
+ res.cookies.set('hello', 'From API')
+ return res
+}
diff --git a/test/e2e/modified-tests/skip-trailing-slash-redirect/app/pages/api/test-cookie.js b/test/e2e/modified-tests/skip-trailing-slash-redirect/app/pages/api/test-cookie.js
new file mode 100644
index 0000000000..4aec0e3eec
--- /dev/null
+++ b/test/e2e/modified-tests/skip-trailing-slash-redirect/app/pages/api/test-cookie.js
@@ -0,0 +1,5 @@
+export default function handler(req, res) {
+ console.log('setting cookie in api route')
+ res.setHeader('Set-Cookie', 'hello=From API')
+ res.status(200).json({ name: 'API' })
+}
diff --git a/test/e2e/modified-tests/skip-trailing-slash-redirect/app/pages/blog/[slug].js b/test/e2e/modified-tests/skip-trailing-slash-redirect/app/pages/blog/[slug].js
new file mode 100644
index 0000000000..d981ff9ea3
--- /dev/null
+++ b/test/e2e/modified-tests/skip-trailing-slash-redirect/app/pages/blog/[slug].js
@@ -0,0 +1,13 @@
+import Link from 'next/link'
+
+export default function Page(props) {
+ return (
+ <>
+ blog page
+
+ to index
+
+
+ >
+ )
+}
diff --git a/test/e2e/modified-tests/skip-trailing-slash-redirect/app/pages/index.js b/test/e2e/modified-tests/skip-trailing-slash-redirect/app/pages/index.js
new file mode 100644
index 0000000000..04fbd07fee
--- /dev/null
+++ b/test/e2e/modified-tests/skip-trailing-slash-redirect/app/pages/index.js
@@ -0,0 +1,17 @@
+import Link from 'next/link'
+
+export default function Page(props) {
+ return (
+ <>
+ index page
+
+ to another
+
+
+
+ to /blog/first
+
+
+ >
+ )
+}
diff --git a/test/e2e/modified-tests/skip-trailing-slash-redirect/index.test.ts b/test/e2e/modified-tests/skip-trailing-slash-redirect/index.test.ts
new file mode 100644
index 0000000000..937b9e2a63
--- /dev/null
+++ b/test/e2e/modified-tests/skip-trailing-slash-redirect/index.test.ts
@@ -0,0 +1,172 @@
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { check, fetchViaHTTP } from 'next-test-utils'
+import { join } from 'path'
+import webdriver from 'next-webdriver'
+const usuallySkip = process.env.RUN_SKIPPED_TESTS ? it : it.skip
+
+describe('skip-trailing-slash-redirect', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: new FileRef(join(__dirname, 'app')),
+ dependencies: {},
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('should allow rewriting invalid buildId correctly', async () => {
+ const res = await fetchViaHTTP(next.url, '/_next/data/missing-id/hello.json', undefined, {
+ headers: {
+ 'x-nextjs-data': '1',
+ },
+ })
+ expect(res.status).toBe(200)
+ expect(await res.text()).toContain('Example Domain')
+
+ if (!(global as any).isNextDeploy) {
+ await check(() => next.cliOutput, /missing-id rewrite/)
+ expect(next.cliOutput).toContain('/_next/data/missing-id/hello.json')
+ }
+ })
+
+ it('should provide original _next/data URL with skipMiddlewareUrlNormalize', async () => {
+ const res = await fetchViaHTTP(next.url, `/_next/data/${next.buildId}/valid.json`, undefined, {
+ headers: {
+ 'x-nextjs-data': '1',
+ },
+ })
+ expect(res.status).toBe(200)
+ expect(await res.text()).toContain('Example Domain')
+ })
+
+ it('should allow response body from middleware with flag', async () => {
+ const res = await fetchViaHTTP(next.url, '/middleware-response-body')
+ expect(res.status).toBe(200)
+ expect(res.headers.get('x-from-middleware')).toBe('true')
+ expect(await res.text()).toBe('hello from middleware')
+ })
+ // NTL Skip
+ usuallySkip('should merge cookies from middleware and API routes correctly', async () => {
+ const res = await fetchViaHTTP(next.url, '/api/test-cookie', undefined, {
+ redirect: 'manual',
+ })
+ expect(res.status).toBe(200)
+ expect(res.headers.get('set-cookie')).toEqual('from-middleware=1; Path=/, hello=From API')
+ })
+ // NTL Skip
+ usuallySkip('should merge cookies from middleware and edge API routes correctly', async () => {
+ const res = await fetchViaHTTP(next.url, '/api/test-cookie-edge', undefined, {
+ redirect: 'manual',
+ })
+ expect(res.status).toBe(200)
+ expect(res.headers.get('set-cookie')).toEqual('from-middleware=1; Path=/, hello=From%20API; Path=/')
+ })
+
+ if ((global as any).isNextStart) {
+ it('should not have trailing slash redirects in manifest', async () => {
+ const routesManifest = JSON.parse(await next.readFile('.next/routes-manifest.json'))
+
+ expect(
+ routesManifest.redirects.some((redirect) => {
+ return (
+ redirect.statusCode === 308 && (redirect.destination === '/:path+' || redirect.destination === '/:path+/')
+ )
+ }),
+ ).toBe(false)
+ })
+ }
+
+ it('should correct skip URL normalizing in middleware', async () => {
+ let res = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/middleware-rewrite-with-slash.json`,
+ undefined,
+ { redirect: 'manual', headers: { 'x-nextjs-data': '1' } },
+ )
+ expect(res.headers.get('x-nextjs-rewrite').endsWith('/another/')).toBe(true)
+
+ res = await fetchViaHTTP(next.url, `/_next/data/${next.buildId}/middleware-rewrite-without-slash.json`, undefined, {
+ redirect: 'manual',
+ headers: { 'x-nextjs-data': '1' },
+ })
+ expect(res.headers.get('x-nextjs-rewrite').endsWith('/another')).toBe(true)
+
+ res = await fetchViaHTTP(next.url, '/middleware-redirect-external-with', undefined, { redirect: 'manual' })
+ expect(res.status).toBe(307)
+ expect(res.headers.get('Location')).toBe('https://example.vercel.sh/somewhere/')
+
+ res = await fetchViaHTTP(next.url, '/middleware-redirect-external-without', undefined, { redirect: 'manual' })
+ expect(res.status).toBe(307)
+ expect(res.headers.get('Location')).toBe('https://example.vercel.sh/somewhere')
+ })
+
+ it('should apply config redirect correctly', async () => {
+ const res = await fetchViaHTTP(next.url, '/redirect-me', undefined, {
+ redirect: 'manual',
+ })
+ expect(res.status).toBe(307)
+ expect(new URL(res.headers.get('location'), 'http://n').pathname).toBe('/another')
+ })
+
+ it('should apply config rewrites correctly', async () => {
+ const res = await fetchViaHTTP(next.url, '/rewrite-me', undefined, {
+ redirect: 'manual',
+ })
+ expect(res.status).toBe(200)
+ expect(await res.text()).toContain('another page')
+ })
+ // NTL Skip
+ usuallySkip('should not apply trailing slash redirect (with slash)', async () => {
+ const res = await fetchViaHTTP(next.url, '/another/', undefined, {
+ redirect: 'manual',
+ })
+ expect(res.status).toBe(200)
+ expect(await res.text()).toContain('another page')
+ })
+
+ it('should not apply trailing slash redirect (without slash)', async () => {
+ const res = await fetchViaHTTP(next.url, '/another', undefined, {
+ redirect: 'manual',
+ })
+ expect(res.status).toBe(200)
+ expect(await res.text()).toContain('another page')
+ })
+
+ it('should respond to index correctly', async () => {
+ const res = await fetchViaHTTP(next.url, '/', undefined, {
+ redirect: 'manual',
+ })
+ expect(res.status).toBe(200)
+ expect(await res.text()).toContain('index page')
+ })
+
+ it('should respond to dynamic route correctly', async () => {
+ const res = await fetchViaHTTP(next.url, '/blog/first', undefined, {
+ redirect: 'manual',
+ })
+ expect(res.status).toBe(200)
+ expect(await res.text()).toContain('blog page')
+ })
+
+ it('should navigate client side correctly', async () => {
+ const browser = await webdriver(next.url, '/')
+
+ expect(await browser.eval('location.pathname')).toBe('/')
+
+ await browser.elementByCss('#to-another').click()
+ await browser.waitForElementByCss('#another')
+
+ expect(await browser.eval('location.pathname')).toBe('/another')
+ await browser.back()
+ await browser.waitForElementByCss('#index')
+
+ expect(await browser.eval('location.pathname')).toBe('/')
+
+ await browser.elementByCss('#to-blog-first').click()
+ await browser.waitForElementByCss('#blog')
+
+ expect(await browser.eval('location.pathname')).toBe('/blog/first')
+ })
+})
diff --git a/test/e2e/modified-tests/streaming-ssr/custom-server/next.config.js b/test/e2e/modified-tests/streaming-ssr/custom-server/next.config.js
new file mode 100644
index 0000000000..4ba52ba2c8
--- /dev/null
+++ b/test/e2e/modified-tests/streaming-ssr/custom-server/next.config.js
@@ -0,0 +1 @@
+module.exports = {}
diff --git a/test/e2e/modified-tests/streaming-ssr/custom-server/pages/index.js b/test/e2e/modified-tests/streaming-ssr/custom-server/pages/index.js
new file mode 100644
index 0000000000..560c19b54a
--- /dev/null
+++ b/test/e2e/modified-tests/streaming-ssr/custom-server/pages/index.js
@@ -0,0 +1,7 @@
+export default function Page() {
+ return streaming
+}
+
+export async function getServerSideProps() {
+ return { props: {} }
+}
diff --git a/test/e2e/modified-tests/streaming-ssr/custom-server/server.js b/test/e2e/modified-tests/streaming-ssr/custom-server/server.js
new file mode 100644
index 0000000000..c8619cd29f
--- /dev/null
+++ b/test/e2e/modified-tests/streaming-ssr/custom-server/server.js
@@ -0,0 +1,43 @@
+const NextServer = require('next/dist/server/next-server').default
+const defaultNextConfig =
+ require('next/dist/server/config-shared').defaultConfig
+const http = require('http')
+
+process.on('SIGTERM', () => process.exit(0))
+process.on('SIGINT', () => process.exit(0))
+
+let handler
+
+const server = http.createServer(async (req, res) => {
+ try {
+ await handler(req, res)
+ } catch (err) {
+ console.error(err)
+ res.statusCode = 500
+ res.end('internal server error')
+ }
+})
+const currentPort = parseInt(process.env.PORT, 10) || 3000
+
+server.listen(currentPort, (err) => {
+ if (err) {
+ console.error('Failed to start server', err)
+ process.exit(1)
+ }
+ const nextServer = new NextServer({
+ hostname: 'localhost',
+ port: currentPort,
+ customServer: true,
+ dev: false,
+ conf: {
+ ...defaultNextConfig,
+ distDir: '.next',
+ experimental: {
+ ...defaultNextConfig.experimental,
+ },
+ },
+ })
+ handler = nextServer.getRequestHandler()
+
+ console.log('Listening on port', currentPort)
+})
diff --git a/test/e2e/modified-tests/streaming-ssr/index.test.ts b/test/e2e/modified-tests/streaming-ssr/index.test.ts
new file mode 100644
index 0000000000..d20d8ac711
--- /dev/null
+++ b/test/e2e/modified-tests/streaming-ssr/index.test.ts
@@ -0,0 +1,180 @@
+import { join } from 'path'
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { fetchViaHTTP, findPort, initNextServerScript, killApp, renderViaHTTP } from 'next-test-utils'
+const usuallySkip = process.env.RUN_SKIPPED_TESTS ? it : it.skip
+
+const react18Deps = {
+ react: '^18.0.0',
+ 'react-dom': '^18.0.0',
+}
+
+const isNextProd = !(global as any).isNextDev && !(global as any).isNextDeploy
+
+describe('react 18 streaming SSR with custom next configs', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ 'app/page.js': `
+ export default function Page() {
+ return 'fake-app' /* this should not enable appDir */
+ }
+ `,
+ pages: new FileRef(join(__dirname, 'streaming-ssr/pages')),
+ },
+ nextConfig: require(join(__dirname, 'streaming-ssr/next.config.js')),
+ dependencies: react18Deps,
+ installCommand: 'npm install',
+ })
+ })
+ afterAll(() => next.destroy())
+ // NTL Skip
+ usuallySkip('should match more specific route along with dynamic routes', async () => {
+ const res1 = await fetchViaHTTP(next.url, '/api/user/login')
+ const res2 = await fetchViaHTTP(next.url, '/api/user/any')
+ expect(await res1.text()).toBe('login')
+ expect(await res2.text()).toBe('[id]')
+ })
+
+ it('should render styled-jsx styles in streaming', async () => {
+ const html = await renderViaHTTP(next.url, '/')
+ expect(html).toContain('color:blue')
+ })
+ // NTL Skip
+ usuallySkip('should redirect paths without trailing-slash and render when slash is appended', async () => {
+ const page = '/hello'
+ const redirectRes = await fetchViaHTTP(next.url, page, {}, { redirect: 'manual' })
+ const res = await fetchViaHTTP(next.url, page + '/')
+ const html = await res.text()
+
+ expect(redirectRes.status).toBe(308)
+ expect(res.status).toBe(200)
+ expect(html).toContain('hello nextjs')
+ expect(html).toContain('home')
+ })
+
+ it('should render next/router correctly in edge runtime', async () => {
+ const html = await renderViaHTTP(next.url, '/router')
+ expect(html).toContain('link')
+ })
+
+ it('should render multi-byte characters correctly in streaming', async () => {
+ const html = await renderViaHTTP(next.url, '/multi-byte')
+ expect(html).toContain('マルチバイト'.repeat(28))
+ })
+})
+
+if (isNextProd) {
+ describe('react 18 streaming SSR with custom server', () => {
+ let next
+ let server
+ let appPort
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ pages: new FileRef(join(__dirname, 'custom-server/pages')),
+ 'server.js': new FileRef(join(__dirname, 'custom-server/server.js')),
+ },
+ nextConfig: require(join(__dirname, 'custom-server/next.config.js')),
+ dependencies: react18Deps,
+ })
+ await next.stop()
+
+ const testServer = join(next.testDir, 'server.js')
+ appPort = await findPort()
+ server = await initNextServerScript(
+ testServer,
+ /Listening/,
+ {
+ ...process.env,
+ PORT: appPort,
+ },
+ undefined,
+ {
+ cwd: next.testDir,
+ },
+ )
+ })
+ afterAll(async () => {
+ await next.destroy()
+ if (server) await killApp(server)
+ })
+ it('should render page correctly under custom server', async () => {
+ const html = await renderViaHTTP(appPort, '/')
+ expect(html).toContain('streaming')
+ })
+ })
+
+ describe('react 18 streaming SSR in minimal mode with node runtime', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ if (isNextProd) {
+ process.env.NEXT_PRIVATE_MINIMAL_MODE = '1'
+ }
+
+ next = await createNext({
+ files: {
+ 'pages/index.js': `
+ export default function Page() {
+ return streaming
+ }
+ export async function getServerSideProps() {
+ return { props: {} }
+ }`,
+ },
+ nextConfig: {
+ experimental: {
+ runtime: 'nodejs',
+ },
+ webpack(config, { nextRuntime }) {
+ const path = require('path')
+ const fs = require('fs')
+
+ const runtimeFilePath = path.join(__dirname, 'runtimes.txt')
+ let runtimeContent = ''
+
+ try {
+ runtimeContent = fs.readFileSync(runtimeFilePath, 'utf8')
+ runtimeContent += '\n'
+ } catch (_) {}
+
+ runtimeContent += nextRuntime || 'client'
+
+ fs.writeFileSync(runtimeFilePath, runtimeContent)
+ return config
+ },
+ },
+ dependencies: react18Deps,
+ })
+ })
+ afterAll(() => {
+ if (isNextProd) {
+ delete process.env.NEXT_PRIVATE_MINIMAL_MODE
+ }
+ next.destroy()
+ })
+
+ it('should pass correct nextRuntime values', async () => {
+ const content = await next.readFile('runtimes.txt')
+ expect(content.split('\n').sort()).toEqual(['client', 'edge', 'nodejs'])
+ })
+
+ it('should generate html response by streaming correctly', async () => {
+ const html = await renderViaHTTP(next.url, '/')
+ expect(html).toContain('streaming')
+ })
+
+ if (isNextProd) {
+ it('should have generated a static 404 page', async () => {
+ expect(await next.readFile('.next/server/pages/404.html')).toBeTruthy()
+
+ const res = await fetchViaHTTP(next.url, '/non-existent')
+ expect(res.status).toBe(404)
+ expect(await res.text()).toContain('This page could not be found')
+ })
+ }
+ })
+}
diff --git a/test/e2e/modified-tests/streaming-ssr/streaming-ssr/next.config.js b/test/e2e/modified-tests/streaming-ssr/streaming-ssr/next.config.js
new file mode 100644
index 0000000000..ce3f975d0e
--- /dev/null
+++ b/test/e2e/modified-tests/streaming-ssr/streaming-ssr/next.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ trailingSlash: true,
+}
diff --git a/test/e2e/modified-tests/streaming-ssr/streaming-ssr/pages/api/user/[id].js b/test/e2e/modified-tests/streaming-ssr/streaming-ssr/pages/api/user/[id].js
new file mode 100644
index 0000000000..c5649d2074
--- /dev/null
+++ b/test/e2e/modified-tests/streaming-ssr/streaming-ssr/pages/api/user/[id].js
@@ -0,0 +1,7 @@
+export default async function handler() {
+ return new Response('[id]')
+}
+
+export const config = {
+ runtime: 'experimental-edge',
+}
diff --git a/test/e2e/modified-tests/streaming-ssr/streaming-ssr/pages/api/user/login.js b/test/e2e/modified-tests/streaming-ssr/streaming-ssr/pages/api/user/login.js
new file mode 100644
index 0000000000..31493ce1ef
--- /dev/null
+++ b/test/e2e/modified-tests/streaming-ssr/streaming-ssr/pages/api/user/login.js
@@ -0,0 +1,3 @@
+export default async function handler(_, res) {
+ res.send('login')
+}
diff --git a/test/e2e/modified-tests/streaming-ssr/streaming-ssr/pages/hello.js b/test/e2e/modified-tests/streaming-ssr/streaming-ssr/pages/hello.js
new file mode 100644
index 0000000000..10d52a4a22
--- /dev/null
+++ b/test/e2e/modified-tests/streaming-ssr/streaming-ssr/pages/hello.js
@@ -0,0 +1,12 @@
+import Link from 'next/link'
+
+export default function Page() {
+ return (
+
+ )
+}
+
+export const config = { runtime: 'experimental-edge' }
diff --git a/test/e2e/modified-tests/streaming-ssr/streaming-ssr/pages/index.js b/test/e2e/modified-tests/streaming-ssr/streaming-ssr/pages/index.js
new file mode 100644
index 0000000000..7d0c7c7738
--- /dev/null
+++ b/test/e2e/modified-tests/streaming-ssr/streaming-ssr/pages/index.js
@@ -0,0 +1,14 @@
+export default function Page() {
+ return (
+
+ )
+}
+
+export const config = { runtime: 'experimental-edge' }
diff --git a/test/e2e/modified-tests/streaming-ssr/streaming-ssr/pages/multi-byte.js b/test/e2e/modified-tests/streaming-ssr/streaming-ssr/pages/multi-byte.js
new file mode 100644
index 0000000000..f92eac96ec
--- /dev/null
+++ b/test/e2e/modified-tests/streaming-ssr/streaming-ssr/pages/multi-byte.js
@@ -0,0 +1,9 @@
+export default function Page() {
+ return (
+
+
{'マルチバイト'.repeat(28)}
+
+ )
+}
+
+export const config = { runtime: 'experimental-edge' }
diff --git a/test/e2e/modified-tests/streaming-ssr/streaming-ssr/pages/router.js b/test/e2e/modified-tests/streaming-ssr/streaming-ssr/pages/router.js
new file mode 100644
index 0000000000..3407d79f07
--- /dev/null
+++ b/test/e2e/modified-tests/streaming-ssr/streaming-ssr/pages/router.js
@@ -0,0 +1,11 @@
+import { useRouter } from 'next/router'
+import Link from 'next/link'
+
+export default () => {
+ useRouter()
+ return link
+}
+
+export const config = {
+ runtime: 'experimental-edge',
+}
diff --git a/test/e2e/next-test-lib/e2e-utils.ts b/test/e2e/next-test-lib/e2e-utils.ts
index f7f71132d4..952eb79888 100644
--- a/test/e2e/next-test-lib/e2e-utils.ts
+++ b/test/e2e/next-test-lib/e2e-utils.ts
@@ -26,7 +26,8 @@ const checkParent = (mod) => {
checkParent(module)
process.env.TEST_FILE_PATH = testFile
-
+// We only test "deploy" on Netlify
+process.env.NEXT_TEST_MODE = 'deploy'
let testMode = 'deploy'
if (!testFileRegex.test(testFile)) {
diff --git a/test/e2e/next-test-lib/next-modes/base.ts b/test/e2e/next-test-lib/next-modes/base.ts
index 7d4802c54a..71800d329e 100644
--- a/test/e2e/next-test-lib/next-modes/base.ts
+++ b/test/e2e/next-test-lib/next-modes/base.ts
@@ -108,6 +108,7 @@ export class NextInstance {
next: process.env.NEXT_TEST_VERSION || require('next/package.json').version,
},
scripts: {
+ build: 'next build',
...pkgScripts,
},
},
@@ -142,7 +143,7 @@ export class NextInstance {
if (!fs.existsSync(path.join(this.testDir, 'netlify.toml'))) {
const toml = /* toml */ `
[build]
- command = "next build"
+ command = "yarn build"
publish = ".next"
[[plugins]]
diff --git a/test/e2e/next-test-lib/next-modes/next-deploy.ts b/test/e2e/next-test-lib/next-modes/next-deploy.ts
index 2cfeab62a0..7d4be7ad64 100644
--- a/test/e2e/next-test-lib/next-modes/next-deploy.ts
+++ b/test/e2e/next-test-lib/next-modes/next-deploy.ts
@@ -1,4 +1,4 @@
-import path from 'path'
+import path, { dirname, relative } from 'path'
import execa from 'execa'
import fs from 'fs-extra'
import { platform } from 'os'
@@ -21,7 +21,7 @@ export class NextDeployInstance extends NextInstance {
this._buildId = 'build-id'
return
}
-
+ const testName = process.env.TEST_FILE_PATH && relative(process.cwd(), process.env.TEST_FILE_PATH)
await super.createTestDir()
// We use yarn because it's better at handling local dependencies
await execa('yarn', [], {
@@ -40,9 +40,11 @@ export class NextDeployInstance extends NextInstance {
}
const NETLIFY_SITE_ID = process.env.NETLIFY_SITE_ID || '1d5a5c76-d445-4ae5-b694-b0d3f2e2c395'
-
+ console.log(`Deploys site for test: ${testName}`)
try {
- const statRes = await execa('ntl', ['status', '--json'], { env: { NETLIFY_SITE_ID, NODE_ENV: 'production' } })
+ const statRes = await execa('ntl', ['status', '--json'], {
+ env: { NETLIFY_SITE_ID, NODE_ENV: 'production' },
+ })
} catch (err) {
if (err.message.includes("You don't appear to be in a folder that is linked to a site")) {
throw new Error(`Site is not linked. Please set "NETLIFY_AUTH_TOKEN" and "NETLIFY_SITE_ID"`)
@@ -52,7 +54,7 @@ export class NextDeployInstance extends NextInstance {
console.log(`Deploying project at ${this.testDir}`)
- const deployRes = await execa('ntl', ['deploy', '--build', '--json'], {
+ const deployRes = await execa('ntl', ['deploy', '--build', '--json', '--message', testName], {
cwd: this.testDir,
reject: false,
env: {
@@ -68,7 +70,7 @@ export class NextDeployInstance extends NextInstance {
try {
const data = JSON.parse(deployRes.stdout)
this._url = data.deploy_url
- console.log(`Deployed to ${this._url}`)
+ console.log(`Deployed to ${this._url}`, data)
this._parsedUrl = new URL(this._url)
} catch (err) {
console.error(err)
diff --git a/test/e2e/next-test-lib/next-test-utils.js b/test/e2e/next-test-lib/next-test-utils.js
index f8e9935c24..d9a63b3595 100644
--- a/test/e2e/next-test-lib/next-test-utils.js
+++ b/test/e2e/next-test-lib/next-test-utils.js
@@ -2,7 +2,8 @@
import spawn from 'cross-spawn'
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'
import { writeFile } from 'fs-extra'
-import { fetch } from 'undici'
+import { fetch as undiciFetch } from 'undici'
+import nodeFetch from 'node-fetch'
import path from 'path'
import qs from 'querystring'
import { TextDecoderStream } from 'stream/web'
@@ -114,18 +115,19 @@ async function processChunkedResponse(response) {
* @returns {Promise}
*/
export function renderViaHTTP(appPort, pathname, query, opts) {
- return fetchViaHTTP(appPort, pathname, query, opts).then(processChunkedResponse)
+ return fetchViaHTTP(appPort, pathname, query, opts, true).then(processChunkedResponse)
}
/**
* @param {string | number} appPort
* @param {string} pathname
- * @param {Record | string | undefined} [query]
- * @param {import("undici").RequestInit} [opts]
- * @returns {Promise}
+ * @param {Record | string | undefined} query
+ * @param {RequestInit} opts
+ * @returns {Promise}
*/
-export function fetchViaHTTP(appPort, pathname, query, opts) {
+export async function fetchViaHTTP(appPort, pathname, query = undefined, opts = undefined, useUndici = false) {
const url = `${pathname}${typeof query === 'string' ? query : query ? `?${qs.stringify(query)}` : ''}`
+ const fetch = useUndici ? undiciFetch : nodeFetch
return fetch(getFullUrl(appPort, url), opts)
}
@@ -376,12 +378,12 @@ export function waitFor(millis) {
}
// check for content in 1 second intervals timing out after
-// 30 seconds
+// 10 seconds
export async function check(contentFn, regex, hardError = true) {
let content
let lastErr
- for (let tries = 0; tries < 30; tries++) {
+ for (let tries = 0; tries < 10; tries++) {
try {
content = await contentFn()
if (typeof regex === 'string') {
@@ -398,11 +400,11 @@ export async function check(contentFn, regex, hardError = true) {
lastErr = err
}
}
- console.error('TIMED OUT CHECK: ', { regex, content, lastErr })
if (hardError) {
throw new Error('TIMED OUT: ' + regex + '\n\n' + content)
}
+ console.error('TIMED OUT CHECK: ', { regex, content, lastErr })
return false
}
diff --git a/test/e2e/tests/dynamic-route-interpolation/index.test.ts b/test/e2e/tests/dynamic-route-interpolation/index.test.ts
new file mode 100644
index 0000000000..a7fb9b8216
--- /dev/null
+++ b/test/e2e/tests/dynamic-route-interpolation/index.test.ts
@@ -0,0 +1,91 @@
+import { createNext } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { renderViaHTTP } from 'next-test-utils'
+import cheerio from 'cheerio'
+import webdriver from 'next-webdriver'
+
+describe('Dynamic Route Interpolation', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ 'pages/blog/[slug].js': `
+ import Link from "next/link"
+ import { useRouter } from "next/router"
+
+ export function getServerSideProps({ params }) {
+ return { props: { slug: params.slug, now: Date.now() } }
+ }
+
+ export default function Page(props) {
+ const router = useRouter()
+ return (
+ <>
+ {props.slug}
+
+ {props.now}
+
+ >
+ )
+ }
+ `,
+
+ 'pages/api/dynamic/[slug].js': `
+ export default function Page(req, res) {
+ const { slug } = req.query
+ res.end('slug: ' + slug)
+ }
+ `,
+ },
+ dependencies: {},
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('should work', async () => {
+ const html = await renderViaHTTP(next.url, '/blog/a')
+ const $ = cheerio.load(html)
+ expect($('#slug').text()).toBe('a')
+ })
+
+ it('should work with parameter itself', async () => {
+ const html = await renderViaHTTP(next.url, '/blog/[slug]')
+ const $ = cheerio.load(html)
+ expect($('#slug').text()).toBe('[slug]')
+ })
+
+ it('should work with brackets', async () => {
+ const html = await renderViaHTTP(next.url, '/blog/[abc]')
+ const $ = cheerio.load(html)
+ expect($('#slug').text()).toBe('[abc]')
+ })
+
+ it('should work with parameter itself in API routes', async () => {
+ const text = await renderViaHTTP(next.url, '/api/dynamic/[slug]')
+ expect(text).toBe('slug: [slug]')
+ })
+
+ it('should work with brackets in API routes', async () => {
+ const text = await renderViaHTTP(next.url, '/api/dynamic/[abc]')
+ expect(text).toBe('slug: [abc]')
+ })
+
+ it('should bust data cache', async () => {
+ const browser = await webdriver(next.url, '/blog/login')
+ await browser.elementById('now').click() // fetch data once
+ const text = await browser.elementById('now').text()
+ await browser.elementById('now').click() // fetch data again
+ await browser.waitForElementByCss(`#now:not(:text("${text}"))`)
+ await browser.close()
+ })
+
+ it('should bust data cache with symbol', async () => {
+ const browser = await webdriver(next.url, '/blog/@login')
+ await browser.elementById('now').click() // fetch data once
+ const text = await browser.elementById('now').text()
+ await browser.elementById('now').click() // fetch data again
+ await browser.waitForElementByCss(`#now:not(:text("${text}"))`)
+ await browser.close()
+ })
+})
diff --git a/test/e2e/tests/edge-api-endpoints-can-receive-body/app/pages/api/edge.js b/test/e2e/tests/edge-api-endpoints-can-receive-body/app/pages/api/edge.js
new file mode 100644
index 0000000000..26bcb1402f
--- /dev/null
+++ b/test/e2e/tests/edge-api-endpoints-can-receive-body/app/pages/api/edge.js
@@ -0,0 +1,11 @@
+export default async (req) => {
+ if (!req.body) {
+ return new Response('Body is required', { status: 400 })
+ }
+
+ return new Response(`got: ${await req.text()}`)
+}
+
+export const config = {
+ runtime: 'experimental-edge',
+}
diff --git a/test/e2e/tests/edge-api-endpoints-can-receive-body/app/pages/api/index.js b/test/e2e/tests/edge-api-endpoints-can-receive-body/app/pages/api/index.js
new file mode 100644
index 0000000000..26bcb1402f
--- /dev/null
+++ b/test/e2e/tests/edge-api-endpoints-can-receive-body/app/pages/api/index.js
@@ -0,0 +1,11 @@
+export default async (req) => {
+ if (!req.body) {
+ return new Response('Body is required', { status: 400 })
+ }
+
+ return new Response(`got: ${await req.text()}`)
+}
+
+export const config = {
+ runtime: 'experimental-edge',
+}
diff --git a/test/e2e/tests/edge-api-endpoints-can-receive-body/index.test.ts b/test/e2e/tests/edge-api-endpoints-can-receive-body/index.test.ts
new file mode 100644
index 0000000000..c33d6b1056
--- /dev/null
+++ b/test/e2e/tests/edge-api-endpoints-can-receive-body/index.test.ts
@@ -0,0 +1,53 @@
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { fetchViaHTTP } from 'next-test-utils'
+import path from 'path'
+
+describe('Edge API endpoints can receive body', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ 'pages/api/edge.js': new FileRef(
+ path.resolve(__dirname, './app/pages/api/edge.js')
+ ),
+ 'pages/api/index.js': new FileRef(
+ path.resolve(__dirname, './app/pages/api/index.js')
+ ),
+ },
+ dependencies: {},
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('reads the body as text', async () => {
+ const res = await fetchViaHTTP(
+ next.url,
+ '/api/edge',
+ {},
+ {
+ body: 'hello, world.',
+ method: 'POST',
+ }
+ )
+
+ expect(res.status).toBe(200)
+ expect(await res.text()).toBe('got: hello, world.')
+ })
+
+ it('reads the body from index', async () => {
+ const res = await fetchViaHTTP(
+ next.url,
+ '/api',
+ {},
+ {
+ body: 'hello, world.',
+ method: 'POST',
+ }
+ )
+
+ expect(res.status).toBe(200)
+ expect(await res.text()).toBe('got: hello, world.')
+ })
+})
diff --git a/test/e2e/tests/edge-compiler-module-exports-preference/index.test.ts b/test/e2e/tests/edge-compiler-module-exports-preference/index.test.ts
new file mode 100644
index 0000000000..b7d2b6abd3
--- /dev/null
+++ b/test/e2e/tests/edge-compiler-module-exports-preference/index.test.ts
@@ -0,0 +1,58 @@
+import { createNext } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { fetchViaHTTP } from 'next-test-utils'
+
+describe('Edge compiler module exports preference', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ 'pages/index.js': `
+ export default function Page() {
+ return hello world
+ }
+ `,
+ 'middleware.js': `
+ import { NextResponse } from 'next/server';
+ import lib from 'my-lib';
+
+ export default (req) => {
+ return NextResponse.next({
+ headers: {
+ 'x-imported': lib
+ }
+ })
+ }
+ `,
+ 'my-lib/package.json': JSON.stringify({
+ name: 'my-lib',
+ version: '1.0.0',
+ main: 'index.js',
+ browser: 'browser.js',
+ }),
+ 'my-lib/index.js': `module.exports = "Node.js"`,
+ 'my-lib/browser.js': `module.exports = "Browser"`,
+ },
+ packageJson: {
+ scripts: {
+ setup: `cp -r ./my-lib ./node_modules`,
+ build: 'yarn setup && next build',
+ dev: 'yarn setup && next dev',
+ start: 'next start',
+ },
+ },
+ startCommand: (global as any).isNextDev ? 'yarn dev' : 'yarn start',
+ buildCommand: 'yarn build',
+ dependencies: {},
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('favors the browser export', async () => {
+ const response = await fetchViaHTTP(next.url, '/')
+ expect(Object.fromEntries(response.headers)).toMatchObject({
+ 'x-imported': 'Browser',
+ })
+ })
+})
diff --git a/test/e2e/getserversideprops/app/next.config.js b/test/e2e/tests/getserversideprops/app/next.config.js
similarity index 100%
rename from test/e2e/getserversideprops/app/next.config.js
rename to test/e2e/tests/getserversideprops/app/next.config.js
diff --git a/test/e2e/getserversideprops/app/pages/500.js b/test/e2e/tests/getserversideprops/app/pages/500.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/500.js
rename to test/e2e/tests/getserversideprops/app/pages/500.js
diff --git a/test/e2e/getserversideprops/app/pages/_app.js b/test/e2e/tests/getserversideprops/app/pages/_app.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/_app.js
rename to test/e2e/tests/getserversideprops/app/pages/_app.js
diff --git a/test/e2e/getserversideprops/app/pages/another/index.js b/test/e2e/tests/getserversideprops/app/pages/another/index.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/another/index.js
rename to test/e2e/tests/getserversideprops/app/pages/another/index.js
diff --git a/test/e2e/getserversideprops/app/pages/blog/[post]/[comment].js b/test/e2e/tests/getserversideprops/app/pages/blog/[post]/[comment].js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/blog/[post]/[comment].js
rename to test/e2e/tests/getserversideprops/app/pages/blog/[post]/[comment].js
diff --git a/test/e2e/getserversideprops/app/pages/blog/[post]/index.js b/test/e2e/tests/getserversideprops/app/pages/blog/[post]/index.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/blog/[post]/index.js
rename to test/e2e/tests/getserversideprops/app/pages/blog/[post]/index.js
diff --git a/test/e2e/getserversideprops/app/pages/blog/index.js b/test/e2e/tests/getserversideprops/app/pages/blog/index.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/blog/index.js
rename to test/e2e/tests/getserversideprops/app/pages/blog/index.js
diff --git a/test/e2e/getserversideprops/app/pages/catchall/[...path].js b/test/e2e/tests/getserversideprops/app/pages/catchall/[...path].js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/catchall/[...path].js
rename to test/e2e/tests/getserversideprops/app/pages/catchall/[...path].js
diff --git a/test/e2e/getserversideprops/app/pages/custom-cache.js b/test/e2e/tests/getserversideprops/app/pages/custom-cache.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/custom-cache.js
rename to test/e2e/tests/getserversideprops/app/pages/custom-cache.js
diff --git a/test/e2e/getserversideprops/app/pages/default-revalidate.js b/test/e2e/tests/getserversideprops/app/pages/default-revalidate.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/default-revalidate.js
rename to test/e2e/tests/getserversideprops/app/pages/default-revalidate.js
diff --git a/test/e2e/getserversideprops/app/pages/early-request-end.js b/test/e2e/tests/getserversideprops/app/pages/early-request-end.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/early-request-end.js
rename to test/e2e/tests/getserversideprops/app/pages/early-request-end.js
diff --git a/test/e2e/getserversideprops/app/pages/enoent.js b/test/e2e/tests/getserversideprops/app/pages/enoent.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/enoent.js
rename to test/e2e/tests/getserversideprops/app/pages/enoent.js
diff --git a/test/e2e/getserversideprops/app/pages/index.js b/test/e2e/tests/getserversideprops/app/pages/index.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/index.js
rename to test/e2e/tests/getserversideprops/app/pages/index.js
diff --git a/test/e2e/getserversideprops/app/pages/invalid-keys.js b/test/e2e/tests/getserversideprops/app/pages/invalid-keys.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/invalid-keys.js
rename to test/e2e/tests/getserversideprops/app/pages/invalid-keys.js
diff --git a/test/e2e/getserversideprops/app/pages/non-json.js b/test/e2e/tests/getserversideprops/app/pages/non-json.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/non-json.js
rename to test/e2e/tests/getserversideprops/app/pages/non-json.js
diff --git a/test/e2e/getserversideprops/app/pages/normal.js b/test/e2e/tests/getserversideprops/app/pages/normal.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/normal.js
rename to test/e2e/tests/getserversideprops/app/pages/normal.js
diff --git a/test/e2e/getserversideprops/app/pages/not-found/[slug].js b/test/e2e/tests/getserversideprops/app/pages/not-found/[slug].js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/not-found/[slug].js
rename to test/e2e/tests/getserversideprops/app/pages/not-found/[slug].js
diff --git a/test/e2e/getserversideprops/app/pages/not-found/index.js b/test/e2e/tests/getserversideprops/app/pages/not-found/index.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/not-found/index.js
rename to test/e2e/tests/getserversideprops/app/pages/not-found/index.js
diff --git a/test/e2e/getserversideprops/app/pages/promise/index.js b/test/e2e/tests/getserversideprops/app/pages/promise/index.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/promise/index.js
rename to test/e2e/tests/getserversideprops/app/pages/promise/index.js
diff --git a/test/e2e/getserversideprops/app/pages/promise/mutate-res-no-streaming.js b/test/e2e/tests/getserversideprops/app/pages/promise/mutate-res-no-streaming.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/promise/mutate-res-no-streaming.js
rename to test/e2e/tests/getserversideprops/app/pages/promise/mutate-res-no-streaming.js
diff --git a/test/e2e/getserversideprops/app/pages/promise/mutate-res-props.js b/test/e2e/tests/getserversideprops/app/pages/promise/mutate-res-props.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/promise/mutate-res-props.js
rename to test/e2e/tests/getserversideprops/app/pages/promise/mutate-res-props.js
diff --git a/test/e2e/getserversideprops/app/pages/promise/mutate-res.js b/test/e2e/tests/getserversideprops/app/pages/promise/mutate-res.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/promise/mutate-res.js
rename to test/e2e/tests/getserversideprops/app/pages/promise/mutate-res.js
diff --git a/test/e2e/getserversideprops/app/pages/refresh.js b/test/e2e/tests/getserversideprops/app/pages/refresh.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/refresh.js
rename to test/e2e/tests/getserversideprops/app/pages/refresh.js
diff --git a/test/e2e/getserversideprops/app/pages/slow/index.js b/test/e2e/tests/getserversideprops/app/pages/slow/index.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/slow/index.js
rename to test/e2e/tests/getserversideprops/app/pages/slow/index.js
diff --git a/test/e2e/getserversideprops/app/pages/something.js b/test/e2e/tests/getserversideprops/app/pages/something.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/something.js
rename to test/e2e/tests/getserversideprops/app/pages/something.js
diff --git a/test/e2e/getserversideprops/app/pages/user/[user]/profile.js b/test/e2e/tests/getserversideprops/app/pages/user/[user]/profile.js
similarity index 100%
rename from test/e2e/getserversideprops/app/pages/user/[user]/profile.js
rename to test/e2e/tests/getserversideprops/app/pages/user/[user]/profile.js
diff --git a/test/e2e/getserversideprops/app/world.txt b/test/e2e/tests/getserversideprops/app/world.txt
similarity index 100%
rename from test/e2e/getserversideprops/app/world.txt
rename to test/e2e/tests/getserversideprops/app/world.txt
diff --git a/test/e2e/getserversideprops/test/index.test.ts b/test/e2e/tests/getserversideprops/test/index.test.ts
similarity index 100%
rename from test/e2e/getserversideprops/test/index.test.ts
rename to test/e2e/tests/getserversideprops/test/index.test.ts
diff --git a/test/e2e/tests/handle-non-hoisted-swc-helpers/index.test.ts b/test/e2e/tests/handle-non-hoisted-swc-helpers/index.test.ts
new file mode 100644
index 0000000000..3fdef2e151
--- /dev/null
+++ b/test/e2e/tests/handle-non-hoisted-swc-helpers/index.test.ts
@@ -0,0 +1,38 @@
+import { createNext } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { renderViaHTTP } from 'next-test-utils'
+
+describe('handle-non-hoisted-swc-helpers', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ 'pages/index.js': `
+ export default function Page() {
+ return hello world
+ }
+
+ export function getServerSideProps() {
+ const helper = require('@swc/helpers/lib/_object_spread.js')
+ console.log(helper)
+ return {
+ props: {
+ now: Date.now()
+ }
+ }
+ }
+ `,
+ },
+ installCommand:
+ 'npm install; mkdir -p node_modules/next/node_modules/@swc; mv node_modules/@swc/helpers node_modules/next/node_modules/@swc/',
+ dependencies: {},
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('should work', async () => {
+ const html = await renderViaHTTP(next.url, '/')
+ expect(html).toContain('hello world')
+ })
+})
diff --git a/test/e2e/tests/i18n-api-support/index.test.ts b/test/e2e/tests/i18n-api-support/index.test.ts
new file mode 100644
index 0000000000..25ec819096
--- /dev/null
+++ b/test/e2e/tests/i18n-api-support/index.test.ts
@@ -0,0 +1,74 @@
+import { createNext } from 'e2e-utils'
+import { fetchViaHTTP } from 'next-test-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+
+describe('i18n API support', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ 'pages/api/hello.js': `
+ export default function handler(req, res) {
+ res.end('hello world')
+ }
+ `,
+ 'pages/api/blog/[slug].js': `
+ export default function handler(req, res) {
+ res.end('blog/[slug]')
+ }
+ `,
+ },
+ nextConfig: {
+ i18n: {
+ locales: ['en', 'fr'],
+ defaultLocale: 'en',
+ },
+ async rewrites() {
+ return {
+ beforeFiles: [],
+ afterFiles: [],
+ fallback: [
+ {
+ source: '/api/:path*',
+ destination: 'https://example.vercel.sh/',
+ },
+ ],
+ }
+ },
+ },
+ dependencies: {},
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('should respond to normal API request', async () => {
+ const res = await fetchViaHTTP(next.url, '/api/hello')
+ expect(res.status).toBe(200)
+ expect(await res.text()).toBe('hello world')
+ })
+
+ it('should respond to normal dynamic API request', async () => {
+ const res = await fetchViaHTTP(next.url, '/api/blog/first')
+ expect(res.status).toBe(200)
+ expect(await res.text()).toBe('blog/[slug]')
+ })
+
+ // TODO: re-enable after this is fixed to match on Vercel
+ if (!(global as any).isNextDeploy) {
+ it('should fallback rewrite non-matching API request', async () => {
+ const paths = [
+ '/fr/api/hello',
+ '/en/api/blog/first',
+ '/en/api/non-existent',
+ '/api/non-existent',
+ ]
+
+ for (const path of paths) {
+ const res = await fetchViaHTTP(next.url, path)
+ expect(res.status).toBe(200)
+ expect(await res.text()).toContain('Example Domain')
+ }
+ })
+ }
+})
diff --git a/test/e2e/tests/i18n-ignore-redirect-source-locale/app/pages/newpage.js b/test/e2e/tests/i18n-ignore-redirect-source-locale/app/pages/newpage.js
new file mode 100644
index 0000000000..4a19d965bb
--- /dev/null
+++ b/test/e2e/tests/i18n-ignore-redirect-source-locale/app/pages/newpage.js
@@ -0,0 +1,6 @@
+import { useRouter } from 'next/router'
+
+export default function Page() {
+ const router = useRouter()
+ return {router.locale}
+}
diff --git a/test/e2e/tests/i18n-ignore-redirect-source-locale/redirects-with-basepath.test.ts b/test/e2e/tests/i18n-ignore-redirect-source-locale/redirects-with-basepath.test.ts
new file mode 100644
index 0000000000..1d6598223f
--- /dev/null
+++ b/test/e2e/tests/i18n-ignore-redirect-source-locale/redirects-with-basepath.test.ts
@@ -0,0 +1,91 @@
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { check } from 'next-test-utils'
+import { join } from 'path'
+import webdriver from 'next-webdriver'
+
+const locales = ['', '/en', '/sv', '/nl']
+
+describe('i18n-ignore-redirect-source-locale with basepath', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ pages: new FileRef(join(__dirname, 'app/pages')),
+ },
+ dependencies: {},
+ nextConfig: {
+ basePath: '/basepath',
+ i18n: {
+ locales: ['en', 'sv', 'nl'],
+ defaultLocale: 'en',
+ },
+ async redirects() {
+ return [
+ {
+ source: '/:locale/to-sv',
+ destination: '/sv/newpage',
+ permanent: false,
+ locale: false,
+ },
+ {
+ source: '/:locale/to-en',
+ destination: '/en/newpage',
+ permanent: false,
+ locale: false,
+ },
+ {
+ source: '/:locale/to-slash',
+ destination: '/newpage',
+ permanent: false,
+ locale: false,
+ },
+ {
+ source: '/:locale/to-same',
+ destination: '/:locale/newpage',
+ permanent: false,
+ locale: false,
+ },
+ ]
+ },
+ },
+ })
+ })
+ afterAll(() => next.destroy())
+
+ test.each(locales)(
+ 'get redirected to the new page, from: %s to: sv',
+ async (locale) => {
+ const browser = await webdriver(next.url, `basepath/${locale}/to-sv`)
+ await check(() => browser.elementById('current-locale').text(), 'sv')
+ }
+ )
+
+ test.each(locales)(
+ 'get redirected to the new page, from: %s to: en',
+ async (locale) => {
+ const browser = await webdriver(next.url, `basepath/${locale}/to-en`)
+ await check(() => browser.elementById('current-locale').text(), 'en')
+ }
+ )
+
+ test.each(locales)(
+ 'get redirected to the new page, from: %s to: /',
+ async (locale) => {
+ const browser = await webdriver(next.url, `basepath/${locale}/to-slash`)
+ await check(() => browser.elementById('current-locale').text(), 'en')
+ }
+ )
+
+ test.each(locales)(
+ 'get redirected to the new page, from and to: %s',
+ async (locale) => {
+ const browser = await webdriver(next.url, `basepath/${locale}/to-same`)
+ await check(
+ () => browser.elementById('current-locale').text(),
+ locale === '' ? 'en' : locale.slice(1)
+ )
+ }
+ )
+})
diff --git a/test/e2e/tests/i18n-ignore-redirect-source-locale/redirects.test.ts b/test/e2e/tests/i18n-ignore-redirect-source-locale/redirects.test.ts
new file mode 100644
index 0000000000..25e328d40f
--- /dev/null
+++ b/test/e2e/tests/i18n-ignore-redirect-source-locale/redirects.test.ts
@@ -0,0 +1,90 @@
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { check } from 'next-test-utils'
+import { join } from 'path'
+import webdriver from 'next-webdriver'
+
+const locales = ['', '/en', '/sv', '/nl']
+
+describe('i18n-ignore-redirect-source-locale', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ pages: new FileRef(join(__dirname, 'app/pages')),
+ },
+ dependencies: {},
+ nextConfig: {
+ i18n: {
+ locales: ['en', 'sv', 'nl'],
+ defaultLocale: 'en',
+ },
+ async redirects() {
+ return [
+ {
+ source: '/:locale/to-sv',
+ destination: '/sv/newpage',
+ permanent: false,
+ locale: false,
+ },
+ {
+ source: '/:locale/to-en',
+ destination: '/en/newpage',
+ permanent: false,
+ locale: false,
+ },
+ {
+ source: '/:locale/to-slash',
+ destination: '/newpage',
+ permanent: false,
+ locale: false,
+ },
+ {
+ source: '/:locale/to-same',
+ destination: '/:locale/newpage',
+ permanent: false,
+ locale: false,
+ },
+ ]
+ },
+ },
+ })
+ })
+ afterAll(() => next.destroy())
+
+ test.each(locales)(
+ 'get redirected to the new page, from: %s to: sv',
+ async (locale) => {
+ const browser = await webdriver(next.url, `${locale}/to-sv`)
+ await check(() => browser.elementById('current-locale').text(), 'sv')
+ }
+ )
+
+ test.each(locales)(
+ 'get redirected to the new page, from: %s to: en',
+ async (locale) => {
+ const browser = await webdriver(next.url, `${locale}/to-en`)
+ await check(() => browser.elementById('current-locale').text(), 'en')
+ }
+ )
+
+ test.each(locales)(
+ 'get redirected to the new page, from: %s to: /',
+ async (locale) => {
+ const browser = await webdriver(next.url, `${locale}/to-slash`)
+ await check(() => browser.elementById('current-locale').text(), 'en')
+ }
+ )
+
+ test.each(locales)(
+ 'get redirected to the new page, from and to: %s',
+ async (locale) => {
+ const browser = await webdriver(next.url, `${locale}/to-same`)
+ await check(
+ () => browser.elementById('current-locale').text(),
+ locale === '' ? 'en' : locale.slice(1)
+ )
+ }
+ )
+})
diff --git a/test/e2e/tests/link-with-api-rewrite/app/next.config.js b/test/e2e/tests/link-with-api-rewrite/app/next.config.js
new file mode 100644
index 0000000000..451e3d8fbd
--- /dev/null
+++ b/test/e2e/tests/link-with-api-rewrite/app/next.config.js
@@ -0,0 +1,17 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ reactStrictMode: true,
+ async rewrites() {
+ return {
+ beforeFiles: [
+ {
+ source: '/:path(.*)',
+ has: [{ type: 'query', key: 'json', value: 'true' }],
+ destination: '/api/json?from=/:path',
+ },
+ ],
+ }
+ },
+}
+
+module.exports = nextConfig
diff --git a/test/e2e/tests/link-with-api-rewrite/app/pages/api/json.js b/test/e2e/tests/link-with-api-rewrite/app/pages/api/json.js
new file mode 100644
index 0000000000..ea288af12d
--- /dev/null
+++ b/test/e2e/tests/link-with-api-rewrite/app/pages/api/json.js
@@ -0,0 +1,5 @@
+export default async function handler(req, res) {
+ const from = req.query.from || ''
+
+ return res.json({ from })
+}
diff --git a/test/e2e/tests/link-with-api-rewrite/app/pages/index.js b/test/e2e/tests/link-with-api-rewrite/app/pages/index.js
new file mode 100644
index 0000000000..a5324fc832
--- /dev/null
+++ b/test/e2e/tests/link-with-api-rewrite/app/pages/index.js
@@ -0,0 +1,14 @@
+import Link from 'next/link'
+
+export default function Page() {
+ return (
+
+
+ to /some/route/for?json=true
+
+
+ to /api/json
+
+
+ )
+}
diff --git a/test/e2e/tests/link-with-api-rewrite/index.test.ts b/test/e2e/tests/link-with-api-rewrite/index.test.ts
new file mode 100644
index 0000000000..31fccb1a42
--- /dev/null
+++ b/test/e2e/tests/link-with-api-rewrite/index.test.ts
@@ -0,0 +1,74 @@
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { check } from 'next-test-utils'
+import { join } from 'path'
+import webdriver from 'next-webdriver'
+
+describe('link-with-api-rewrite', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ pages: new FileRef(join(__dirname, 'app/pages')),
+ 'next.config.js': new FileRef(join(__dirname, 'app/next.config.js')),
+ },
+ dependencies: {},
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('should perform hard navigation for rewritten urls', async () => {
+ const browser = await webdriver(next.url, '/')
+
+ try {
+ // Click the link on the page, we expect that there will be a hard
+ // navigation later (we do this be checking that the window global is
+ // unset).
+ await browser.eval('window.beforeNav = "hi"')
+ await browser.elementById('rewrite').click()
+ await check(() => browser.eval('window.beforeNav'), {
+ test: (content) => content !== 'hi',
+ })
+
+ // Check to see that we were in fact navigated to the correct page.
+ const pathname = await browser.eval('window.location.pathname')
+ expect(pathname).toBe('/some/route/for')
+
+ // Check to see that the resulting data is coming from the right endpoint.
+ const text = await browser.eval(
+ 'window.document.documentElement.innerText'
+ )
+ expect(text).toBe('{"from":"/some/route/for"}')
+ } finally {
+ await browser.close()
+ }
+ })
+
+ it('should perform hard navigation for direct urls', async () => {
+ const browser = await webdriver(next.url, '/')
+
+ try {
+ // Click the link on the page, we expect that there will be a hard
+ // navigation later (we do this be checking that the window global is
+ // unset).
+ await browser.eval('window.beforeNav = "hi"')
+ await browser.elementById('direct').click()
+ await check(() => browser.eval('window.beforeNav'), {
+ test: (content) => content !== 'hi',
+ })
+
+ // Check to see that we were in fact navigated to the correct page.
+ const pathname = await browser.eval('window.location.pathname')
+ expect(pathname).toBe('/api/json')
+
+ // Check to see that the resulting data is coming from the right endpoint.
+ const text = await browser.eval(
+ 'window.document.documentElement.innerText'
+ )
+ expect(text).toBe('{"from":""}')
+ } finally {
+ await browser.close()
+ }
+ })
+})
diff --git a/test/e2e/tests/middleware-base-path/app/middleware.js b/test/e2e/tests/middleware-base-path/app/middleware.js
new file mode 100644
index 0000000000..2e1bcab993
--- /dev/null
+++ b/test/e2e/tests/middleware-base-path/app/middleware.js
@@ -0,0 +1,24 @@
+import { NextResponse } from 'next/server'
+
+export async function middleware(request) {
+ const url = request.nextUrl
+
+ if (
+ request.method === 'HEAD' &&
+ url.basePath === '/root' &&
+ url.pathname === '/redirect-me-to-about'
+ ) {
+ url.pathname = '/about'
+ return NextResponse.redirect(url)
+ }
+
+ if (url.pathname === '/redirect-with-basepath' && !url.basePath) {
+ url.basePath = '/root'
+ return NextResponse.redirect(url)
+ }
+
+ if (url.pathname === '/redirect-with-basepath') {
+ url.pathname = '/about'
+ return NextResponse.rewrite(url)
+ }
+}
diff --git a/test/e2e/tests/middleware-base-path/app/next.config.js b/test/e2e/tests/middleware-base-path/app/next.config.js
new file mode 100644
index 0000000000..959fceae74
--- /dev/null
+++ b/test/e2e/tests/middleware-base-path/app/next.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ basePath: '/root',
+}
diff --git a/test/e2e/tests/middleware-base-path/app/pages/about.js b/test/e2e/tests/middleware-base-path/app/pages/about.js
new file mode 100644
index 0000000000..9924acf037
--- /dev/null
+++ b/test/e2e/tests/middleware-base-path/app/pages/about.js
@@ -0,0 +1,7 @@
+export default function About() {
+ return (
+
+
About Page
+
+ )
+}
diff --git a/test/e2e/tests/middleware-base-path/app/pages/dynamic-routes/[routeName].js b/test/e2e/tests/middleware-base-path/app/pages/dynamic-routes/[routeName].js
new file mode 100644
index 0000000000..1fe4784b68
--- /dev/null
+++ b/test/e2e/tests/middleware-base-path/app/pages/dynamic-routes/[routeName].js
@@ -0,0 +1,11 @@
+import { useRouter } from 'next/router'
+
+export default function DynamicRoutes() {
+ const { query } = useRouter()
+
+ return (
+
+ {query.routeName}
+
+ )
+}
diff --git a/test/e2e/tests/middleware-base-path/app/pages/index.js b/test/e2e/tests/middleware-base-path/app/pages/index.js
new file mode 100644
index 0000000000..a65b40f43f
--- /dev/null
+++ b/test/e2e/tests/middleware-base-path/app/pages/index.js
@@ -0,0 +1,41 @@
+import Link from 'next/link'
+
+export default function Main({ message }) {
+ return (
+
+
Hello {message}
+
+
+ Stream a response
+
+
+
+ Rewrite me to about
+
+
+
+ Rewrite me to Vercel
+
+
+ redirect me to about
+
+
+
+ Hello World
+
+
+
+
+ )
+}
+
+export const getServerSideProps = ({ query }) => ({
+ props: { message: query.message || 'World' },
+})
diff --git a/test/e2e/tests/middleware-base-path/test/index.test.ts b/test/e2e/tests/middleware-base-path/test/index.test.ts
new file mode 100644
index 0000000000..dffe7a1e79
--- /dev/null
+++ b/test/e2e/tests/middleware-base-path/test/index.test.ts
@@ -0,0 +1,48 @@
+/* eslint-env jest */
+import { join } from 'path'
+import cheerio from 'cheerio'
+import webdriver from 'next-webdriver'
+import { fetchViaHTTP } from 'next-test-utils'
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+
+describe('Middleware base tests', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ pages: new FileRef(join(__dirname, '../app/pages')),
+ 'middleware.js': new FileRef(join(__dirname, '../app/middleware.js')),
+ 'next.config.js': new FileRef(join(__dirname, '../app/next.config.js')),
+ },
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('should execute from absolute paths', async () => {
+ const browser = await webdriver(next.url, '/redirect-with-basepath')
+ try {
+ expect(await browser.eval(`window.location.pathname`)).toBe(
+ '/root/redirect-with-basepath'
+ )
+ } finally {
+ await browser.close()
+ }
+
+ const res = await fetchViaHTTP(next.url, '/root/redirect-with-basepath')
+ const html = await res.text()
+ const $ = cheerio.load(html)
+ expect($('.title').text()).toBe('About Page')
+ })
+ it('router.query must exist when Link clicked page routing', async () => {
+ const browser = await webdriver(next.url, '/root')
+ try {
+ await browser.elementById('go-to-hello-world-anchor').click()
+ const routeName = await browser.elementById('route-name').text()
+ expect(routeName).toMatch('hello-world')
+ } finally {
+ await browser.close()
+ }
+ })
+})
diff --git a/test/e2e/tests/middleware-custom-matchers-i18n/app/middleware.js b/test/e2e/tests/middleware-custom-matchers-i18n/app/middleware.js
new file mode 100644
index 0000000000..bdd67e48df
--- /dev/null
+++ b/test/e2e/tests/middleware-custom-matchers-i18n/app/middleware.js
@@ -0,0 +1,21 @@
+import { NextResponse } from 'next/server'
+
+export default function middleware(request) {
+ const nextUrl = request.nextUrl.clone()
+ nextUrl.pathname = '/'
+ const res = NextResponse.rewrite(nextUrl)
+ res.headers.set('X-From-Middleware', 'true')
+ return res
+}
+
+export const config = {
+ matcher: [
+ {
+ source: '/hello',
+ },
+ {
+ source: '/nl-NL/about',
+ locale: false,
+ },
+ ],
+}
diff --git a/test/e2e/tests/middleware-custom-matchers-i18n/app/next.config.js b/test/e2e/tests/middleware-custom-matchers-i18n/app/next.config.js
new file mode 100644
index 0000000000..97c6addb6f
--- /dev/null
+++ b/test/e2e/tests/middleware-custom-matchers-i18n/app/next.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ i18n: {
+ locales: ['en', 'nl-NL'],
+ defaultLocale: 'en',
+ },
+}
diff --git a/test/e2e/tests/middleware-custom-matchers-i18n/app/pages/index.js b/test/e2e/tests/middleware-custom-matchers-i18n/app/pages/index.js
new file mode 100644
index 0000000000..accd5b17ad
--- /dev/null
+++ b/test/e2e/tests/middleware-custom-matchers-i18n/app/pages/index.js
@@ -0,0 +1,14 @@
+export default (props) => (
+ <>
+ home
+ {props.fromMiddleware}
+ >
+)
+
+export async function getServerSideProps({ res }) {
+ return {
+ props: {
+ fromMiddleware: res.getHeader('x-from-middleware') || null,
+ },
+ }
+}
diff --git a/test/e2e/tests/middleware-custom-matchers-i18n/app/pages/routes.js b/test/e2e/tests/middleware-custom-matchers-i18n/app/pages/routes.js
new file mode 100644
index 0000000000..de7469fb8b
--- /dev/null
+++ b/test/e2e/tests/middleware-custom-matchers-i18n/app/pages/routes.js
@@ -0,0 +1,26 @@
+import Link from 'next/link'
+
+export default (props) => (
+
+
+
+ /hello
+
+
+
+
+ /en/hello
+
+
+
+
+ /nl-NL/hello
+
+
+
+
+ /nl-NL/about
+
+
+
+)
diff --git a/test/e2e/tests/middleware-custom-matchers-i18n/test/index.test.ts b/test/e2e/tests/middleware-custom-matchers-i18n/test/index.test.ts
new file mode 100644
index 0000000000..0d81f50c74
--- /dev/null
+++ b/test/e2e/tests/middleware-custom-matchers-i18n/test/index.test.ts
@@ -0,0 +1,51 @@
+/* eslint-env jest */
+/* eslint-disable jest/no-standalone-expect */
+
+import { join } from 'path'
+import webdriver from 'next-webdriver'
+import { fetchViaHTTP } from 'next-test-utils'
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+
+const itif = (condition: boolean) => (condition ? it : it.skip)
+
+const isModeDeploy = process.env.NEXT_TEST_MODE === 'deploy'
+
+describe('Middleware custom matchers i18n', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: new FileRef(join(__dirname, '../app')),
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it.each(['/hello', '/en/hello', '/nl-NL/hello', '/nl-NL/about'])('should match', async (path) => {
+ const res = await fetchViaHTTP(next.url, path)
+ expect(res.status).toBe(200)
+ expect(res.headers.get('x-from-middleware')).toBeDefined()
+ })
+
+ // NTL Fail - it thinks "invalid" is a valid locale
+ // it.each(['/invalid/hello', '/hello/invalid', '/about', '/en/about'])(
+ it.each(['/hello/invalid', '/about', '/en/about'])('should not match', async (path) => {
+ const res = await fetchViaHTTP(next.url, path)
+ expect(res.status).toBe(404)
+ })
+
+ // FIXME:
+ // See https://linear.app/vercel/issue/EC-160/header-value-set-on-middleware-is-not-propagated-on-client-request-of
+ itif(!isModeDeploy).each(['hello', 'en_hello', 'nl-NL_hello', 'nl-NL_about'])(
+ 'should match has query on client routing',
+ async (id) => {
+ const browser = await webdriver(next.url, '/routes')
+ await browser.eval('window.__TEST_NO_RELOAD = true')
+ await browser.elementById(id).click()
+ const fromMiddleware = await browser.elementById('from-middleware').text()
+ expect(fromMiddleware).toBe('true')
+ const noReload = await browser.eval('window.__TEST_NO_RELOAD')
+ expect(noReload).toBe(true)
+ },
+ )
+})
diff --git a/test/e2e/tests/middleware-fetches-with-any-http-method/index.test.ts b/test/e2e/tests/middleware-fetches-with-any-http-method/index.test.ts
new file mode 100644
index 0000000000..fd8be9a47f
--- /dev/null
+++ b/test/e2e/tests/middleware-fetches-with-any-http-method/index.test.ts
@@ -0,0 +1,91 @@
+import { createNext } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { fetchViaHTTP } from 'next-test-utils'
+
+describe('Middleware fetches with any HTTP method', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ 'pages/api/ping.js': `
+ export default (req, res) => {
+ res.send(JSON.stringify({
+ method: req.method,
+ headers: {...req.headers},
+ }))
+ }
+ `,
+ 'middleware.js': `
+ import { NextResponse } from 'next/server';
+
+ const HTTP_ECHO_URL = 'https://http-echo-kou029w.vercel.app/';
+
+ export default async (req) => {
+ const kind = req.nextUrl.searchParams.get('kind')
+ const handler = handlers[kind] ?? handlers['normal-fetch'];
+
+ const response = await handler({url: HTTP_ECHO_URL, method: req.method});
+ const json = await response.text()
+
+ const res = NextResponse.next();
+ res.headers.set('x-resolved', json ?? '{}');
+ return res
+ }
+
+ const handlers = {
+ 'new-request': ({url, method}) =>
+ fetch(new Request(url, { method, headers: { 'x-kind': 'new-request' } })),
+
+ 'normal-fetch': ({url, method}) =>
+ fetch(url, { method, headers: { 'x-kind': 'normal-fetch' } })
+ }
+ `,
+ },
+ dependencies: {},
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('passes the method on a direct fetch request', async () => {
+ const response = await fetchViaHTTP(
+ next.url,
+ '/api/ping',
+ {},
+ { method: 'POST' }
+ )
+ const json = await response.json()
+ expect(json).toMatchObject({
+ method: 'POST',
+ })
+
+ const headerJson = JSON.parse(response.headers.get('x-resolved'))
+ expect(headerJson).toMatchObject({
+ method: 'POST',
+ headers: {
+ 'x-kind': 'normal-fetch',
+ },
+ })
+ })
+
+ it('passes the method when providing a Request object', async () => {
+ const response = await fetchViaHTTP(
+ next.url,
+ '/api/ping',
+ { kind: 'new-request' },
+ { method: 'POST' }
+ )
+ const json = await response.json()
+ expect(json).toMatchObject({
+ method: 'POST',
+ })
+
+ const headerJson = JSON.parse(response.headers.get('x-resolved'))
+ expect(headerJson).toMatchObject({
+ method: 'POST',
+ headers: {
+ 'x-kind': 'new-request',
+ },
+ })
+ })
+})
diff --git a/test/e2e/tests/middleware-matcher/app/middleware.js b/test/e2e/tests/middleware-matcher/app/middleware.js
new file mode 100644
index 0000000000..546a09b541
--- /dev/null
+++ b/test/e2e/tests/middleware-matcher/app/middleware.js
@@ -0,0 +1,17 @@
+import { NextResponse } from 'next/server'
+
+export const config = {
+ matcher: [
+ '/',
+ '/with-middleware/:path*',
+ '/another-middleware/:path*',
+ // the below is testing special characters don't break the build
+ '/_sites/:path((?![^/]*\\.json$)[^/]+$)',
+ ],
+}
+
+export default (req) => {
+ const res = NextResponse.next()
+ res.headers.set('X-From-Middleware', 'true')
+ return res
+}
diff --git a/test/e2e/tests/middleware-matcher/app/pages/another-middleware.js b/test/e2e/tests/middleware-matcher/app/pages/another-middleware.js
new file mode 100644
index 0000000000..57d698e054
--- /dev/null
+++ b/test/e2e/tests/middleware-matcher/app/pages/another-middleware.js
@@ -0,0 +1,22 @@
+import Link from 'next/link'
+
+export default function Page(props) {
+ return (
+
+
This should also run the middleware
+
{JSON.stringify(props)}
+
+ to /
+
+
+
+ )
+}
+
+export const getServerSideProps = () => {
+ return {
+ props: {
+ message: 'Hello, magnificent world.',
+ },
+ }
+}
diff --git a/test/e2e/tests/middleware-matcher/app/pages/blog/[slug].js b/test/e2e/tests/middleware-matcher/app/pages/blog/[slug].js
new file mode 100644
index 0000000000..f718a7daf6
--- /dev/null
+++ b/test/e2e/tests/middleware-matcher/app/pages/blog/[slug].js
@@ -0,0 +1,27 @@
+import Link from 'next/link'
+
+export default function Page(props) {
+ return (
+
+
This should not run the middleware
+
{JSON.stringify(props)}
+
+ to /another-middleware
+
+
+
+ to /blog/slug-2
+
+
+
+ )
+}
+
+export const getServerSideProps = ({ params }) => {
+ return {
+ props: {
+ params,
+ message: 'Hello, magnificent world.',
+ },
+ }
+}
diff --git a/test/e2e/tests/middleware-matcher/app/pages/index.js b/test/e2e/tests/middleware-matcher/app/pages/index.js
new file mode 100644
index 0000000000..5a99c14917
--- /dev/null
+++ b/test/e2e/tests/middleware-matcher/app/pages/index.js
@@ -0,0 +1,26 @@
+import Link from 'next/link'
+
+export default function Page(props) {
+ return (
+
+
root page
+
{JSON.stringify(props)}
+
+ to /another-middleware
+
+
+
+ to /blog/slug-1
+
+
+
+ )
+}
+
+export const getServerSideProps = () => {
+ return {
+ props: {
+ message: 'Hello, world.',
+ },
+ }
+}
diff --git a/test/e2e/tests/middleware-matcher/app/pages/with-middleware.js b/test/e2e/tests/middleware-matcher/app/pages/with-middleware.js
new file mode 100644
index 0000000000..da078df25f
--- /dev/null
+++ b/test/e2e/tests/middleware-matcher/app/pages/with-middleware.js
@@ -0,0 +1,16 @@
+export default function Page({ message }) {
+ return (
+
+
This should run the middleware
+
{message}
+
+ )
+}
+
+export const getServerSideProps = () => {
+ return {
+ props: {
+ message: 'Hello, cruel world.',
+ },
+ }
+}
diff --git a/test/e2e/tests/middleware-matcher/index.test.ts b/test/e2e/tests/middleware-matcher/index.test.ts
new file mode 100644
index 0000000000..2049c560eb
--- /dev/null
+++ b/test/e2e/tests/middleware-matcher/index.test.ts
@@ -0,0 +1,530 @@
+/* eslint-disable jest/no-identical-title */
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { check, fetchViaHTTP } from 'next-test-utils'
+import { join } from 'path'
+import webdriver from 'next-webdriver'
+
+describe('Middleware can set the matcher in its config', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: new FileRef(join(__dirname, 'app')),
+ dependencies: {},
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('does add the header for root request', async () => {
+ const response = await fetchViaHTTP(next.url, '/')
+ expect(response.headers.get('X-From-Middleware')).toBe('true')
+ expect(await response.text()).toContain('root page')
+ })
+
+ it('adds the header for a matched path', async () => {
+ const response = await fetchViaHTTP(next.url, '/with-middleware')
+ expect(response.headers.get('X-From-Middleware')).toBe('true')
+ expect(await response.text()).toContain('This should run the middleware')
+ })
+
+ it('adds the header for a matched data path (with header)', async () => {
+ const response = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/with-middleware.json`,
+ undefined,
+ { headers: { 'x-nextjs-data': '1' } }
+ )
+ expect(await response.json()).toMatchObject({
+ pageProps: {
+ message: 'Hello, cruel world.',
+ },
+ })
+ expect(response.headers.get('X-From-Middleware')).toBe('true')
+ })
+
+ it('adds the header for a matched data path (without header)', async () => {
+ const response = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/with-middleware.json`
+ )
+ expect(await response.json()).toMatchObject({
+ pageProps: {
+ message: 'Hello, cruel world.',
+ },
+ })
+ expect(response.headers.get('X-From-Middleware')).toBe('true')
+ })
+
+ it('adds the header for another matched path', async () => {
+ const response = await fetchViaHTTP(next.url, '/another-middleware')
+ expect(response.headers.get('X-From-Middleware')).toBe('true')
+ expect(await response.text()).toContain(
+ 'This should also run the middleware'
+ )
+ })
+
+ it('adds the header for another matched data path', async () => {
+ const response = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/another-middleware.json`,
+ undefined,
+ { headers: { 'x-nextjs-data': '1' } }
+ )
+ expect(await response.json()).toMatchObject({
+ pageProps: {
+ message: 'Hello, magnificent world.',
+ },
+ })
+ expect(response.headers.get('X-From-Middleware')).toBe('true')
+ })
+
+ it('does add the header for root data request', async () => {
+ const response = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/index.json`,
+ undefined,
+ { headers: { 'x-nextjs-data': '1' } }
+ )
+ expect(await response.json()).toMatchObject({
+ pageProps: {
+ message: 'Hello, world.',
+ },
+ })
+ expect(response.headers.get('X-From-Middleware')).toBe('true')
+ })
+
+ it('should load matches in client matchers correctly', async () => {
+ const browser = await webdriver(next.url, '/')
+
+ await check(async () => {
+ const matchers = await browser.eval(
+ (global as any).isNextDev
+ ? 'window.__DEV_MIDDLEWARE_MATCHERS'
+ : 'window.__MIDDLEWARE_MATCHERS'
+ )
+
+ return matchers &&
+ matchers.some((m) => m.regexp.includes('with-middleware')) &&
+ matchers.some((m) => m.regexp.includes('another-middleware'))
+ ? 'success'
+ : 'failed'
+ }, 'success')
+ })
+
+ it('should navigate correctly with matchers', async () => {
+ const browser = await webdriver(next.url, '/')
+ await browser.eval('window.beforeNav = 1')
+
+ await browser.elementByCss('#to-another-middleware').click()
+ await browser.waitForElementByCss('#another-middleware')
+
+ expect(JSON.parse(await browser.elementByCss('#props').text())).toEqual({
+ message: 'Hello, magnificent world.',
+ })
+
+ await browser.elementByCss('#to-index').click()
+ await browser.waitForElementByCss('#index')
+
+ await browser.elementByCss('#to-blog-slug-1').click()
+ await browser.waitForElementByCss('#blog')
+ expect(JSON.parse(await browser.elementByCss('#props').text())).toEqual({
+ message: 'Hello, magnificent world.',
+ params: {
+ slug: 'slug-1',
+ },
+ })
+
+ await browser.elementByCss('#to-blog-slug-2').click()
+ await check(
+ () => browser.eval('document.documentElement.innerHTML'),
+ /"slug":"slug-2"/
+ )
+ expect(JSON.parse(await browser.elementByCss('#props').text())).toEqual({
+ message: 'Hello, magnificent world.',
+ params: {
+ slug: 'slug-2',
+ },
+ })
+ })
+})
+
+describe('using a single matcher', () => {
+ let next: NextInstance
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ 'pages/[...route].js': `
+ export default function Page({ message }) {
+ return
+
root page
+
{message}
+
+ }
+
+ export const getServerSideProps = ({ params }) => {
+ return {
+ props: {
+ message: "Hello from /" + params.route.join("/")
+ }
+ }
+ }
+ `,
+ 'middleware.js': `
+ import { NextResponse } from 'next/server'
+ export const config = {
+ matcher: '/middleware/works'
+ };
+ export default (req) => {
+ const res = NextResponse.next();
+ res.headers.set('X-From-Middleware', 'true');
+ return res;
+ }
+ `,
+ },
+ dependencies: {},
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('does not add the header for root request', async () => {
+ const response = await fetchViaHTTP(next.url, '/')
+ expect(response.headers.get('X-From-Middleware')).toBeFalsy()
+ })
+
+ it('does not add the header for root data request', async () => {
+ const response = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/index.json`,
+ undefined,
+ { headers: { 'x-nextjs-data': '1' } }
+ )
+ expect(response.headers.get('X-From-Middleware')).toBeFalsy()
+ })
+
+ it('adds the header for a matched path', async () => {
+ const response = await fetchViaHTTP(next.url, '/middleware/works')
+ expect(await response.text()).toContain('Hello from /middleware/works')
+ expect(response.headers.get('X-From-Middleware')).toBe('true')
+ })
+
+ it('adds the headers for a matched data path (with header)', async () => {
+ const response = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/middleware/works.json`,
+ undefined,
+ { headers: { 'x-nextjs-data': '1' } }
+ )
+ expect(await response.json()).toMatchObject({
+ pageProps: {
+ message: 'Hello from /middleware/works',
+ },
+ })
+ expect(response.headers.get('X-From-Middleware')).toBe('true')
+ })
+
+ it('adds the header for a matched data path (without header)', async () => {
+ const response = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/middleware/works.json`
+ )
+ expect(await response.json()).toMatchObject({
+ pageProps: {
+ message: 'Hello from /middleware/works',
+ },
+ })
+ expect(response.headers.get('X-From-Middleware')).toBe('true')
+ })
+
+ it('does not add the header for an unmatched path', async () => {
+ const response = await fetchViaHTTP(next.url, '/about/me')
+ expect(await response.text()).toContain('Hello from /about/me')
+ expect(response.headers.get('X-From-Middleware')).toBeNull()
+ })
+})
+
+describe('using root matcher', () => {
+ let next: NextInstance
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ 'pages/index.js': `
+ export function getStaticProps() {
+ return {
+ props: {
+ message: 'hello world'
+ }
+ }
+ }
+
+ export default function Home({ message }) {
+ return Hi there!
+ }
+ `,
+ 'middleware.js': `
+ import { NextResponse } from 'next/server'
+ export default (req) => {
+ const res = NextResponse.next();
+ res.headers.set('X-From-Middleware', 'true');
+ return res;
+ }
+
+ export const config = { matcher: '/' };
+ `,
+ },
+ dependencies: {},
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('adds the header to the /', async () => {
+ const response = await fetchViaHTTP(next.url, '/')
+ expect(response.status).toBe(200)
+ expect(Object.fromEntries(response.headers)).toMatchObject({
+ 'x-from-middleware': 'true',
+ })
+ })
+
+ it('adds the header to the /index', async () => {
+ const response = await fetchViaHTTP(next.url, '/index')
+ expect(Object.fromEntries(response.headers)).toMatchObject({
+ 'x-from-middleware': 'true',
+ })
+ })
+
+ it('adds the header for a matched data path (with header)', async () => {
+ const response = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/index.json`,
+ undefined,
+ { headers: { 'x-nextjs-data': '1' } }
+ )
+ expect(await response.json()).toMatchObject({
+ pageProps: {
+ message: 'hello world',
+ },
+ })
+ expect(response.headers.get('X-From-Middleware')).toBe('true')
+ })
+
+ it('adds the header for a matched data path (without header)', async () => {
+ const response = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/index.json`
+ )
+ expect(await response.json()).toMatchObject({
+ pageProps: {
+ message: 'hello world',
+ },
+ })
+ expect(response.headers.get('X-From-Middleware')).toBe('true')
+ })
+})
+
+describe.each([
+ { title: '' },
+ { title: ' and trailingSlash', trailingSlash: true },
+])('using a single matcher with i18n$title', ({ trailingSlash }) => {
+ let next: NextInstance
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ 'pages/index.js': `
+ export default function Page({ message }) {
+ return
+ }
+ export const getServerSideProps = ({ params, locale }) => ({
+ props: { message: \`(\${locale}) Hello from /\` }
+ })
+ `,
+ 'pages/[...route].js': `
+ export default function Page({ message }) {
+ return
+
catchall page
+
{message}
+
+ }
+ export const getServerSideProps = ({ params, locale }) => ({
+ props: { message: \`(\${locale}) Hello from /\` + params.route.join("/") }
+ })
+ `,
+ 'middleware.js': `
+ import { NextResponse } from 'next/server'
+ export const config = { matcher: '/' };
+ export default (req) => {
+ const res = NextResponse.next();
+ res.headers.set('X-From-Middleware', 'true');
+ return res;
+ }
+ `,
+ 'next.config.js': `
+ module.exports = {
+ ${trailingSlash ? 'trailingSlash: true,' : ''}
+ i18n: {
+ localeDetection: false,
+ locales: ['es', 'en'],
+ defaultLocale: 'en',
+ }
+ }
+ `,
+ },
+ dependencies: {},
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it(`adds the header for a matched path`, async () => {
+ const res1 = await fetchViaHTTP(next.url, `/`)
+ expect(await res1.text()).toContain(`(en) Hello from /`)
+ expect(res1.headers.get('X-From-Middleware')).toBe('true')
+ const res2 = await fetchViaHTTP(next.url, `/es`)
+ expect(await res2.text()).toContain(`(es) Hello from /`)
+ expect(res2.headers.get('X-From-Middleware')).toBe('true')
+ })
+
+ it('adds the header for a mathed root path with /index', async () => {
+ const res1 = await fetchViaHTTP(next.url, `/index`)
+ expect(await res1.text()).toContain(`(en) Hello from /`)
+ expect(res1.headers.get('X-From-Middleware')).toBe('true')
+ const res2 = await fetchViaHTTP(next.url, `/es/index`)
+ expect(await res2.text()).toContain(`(es) Hello from /`)
+ expect(res2.headers.get('X-From-Middleware')).toBe('true')
+ })
+
+ it(`adds the headers for a matched data path`, async () => {
+ const res1 = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/en.json`,
+ undefined,
+ { headers: { 'x-nextjs-data': '1' } }
+ )
+ expect(await res1.json()).toMatchObject({
+ pageProps: { message: `(en) Hello from /` },
+ })
+ expect(res1.headers.get('X-From-Middleware')).toBe('true')
+ const res2 = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/es.json`,
+ undefined
+ )
+ expect(await res2.json()).toMatchObject({
+ pageProps: { message: `(es) Hello from /` },
+ })
+ expect(res2.headers.get('X-From-Middleware')).toBe('true')
+ })
+
+ it(`does not add the header for an unmatched path`, async () => {
+ const response = await fetchViaHTTP(next.url, `/about/me`)
+ expect(await response.text()).toContain('Hello from /about/me')
+ expect(response.headers.get('X-From-Middleware')).toBeNull()
+ })
+})
+
+describe.each([
+ { title: '' },
+ { title: ' and trailingSlash', trailingSlash: true },
+])(
+ 'using a single matcher with i18n and basePath$title',
+ ({ trailingSlash }) => {
+ let next: NextInstance
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ 'pages/index.js': `
+ export default function Page({ message }) {
+ return
+
root page
+
{message}
+
+ }
+ export const getServerSideProps = ({ params, locale }) => ({
+ props: { message: \`(\${locale}) Hello from /\` }
+ })
+ `,
+ 'pages/[...route].js': `
+ export default function Page({ message }) {
+ return
+
catchall page
+
{message}
+
+ }
+ export const getServerSideProps = ({ params, locale }) => ({
+ props: { message: \`(\${locale}) Hello from /\` + params.route.join("/") }
+ })
+ `,
+ 'middleware.js': `
+ import { NextResponse } from 'next/server'
+ export const config = { matcher: '/' };
+ export default (req) => {
+ const res = NextResponse.next();
+ res.headers.set('X-From-Middleware', 'true');
+ return res;
+ }
+ `,
+ 'next.config.js': `
+ module.exports = {
+ ${trailingSlash ? 'trailingSlash: true,' : ''}
+ basePath: '/root',
+ i18n: {
+ localeDetection: false,
+ locales: ['es', 'en'],
+ defaultLocale: 'en',
+ }
+ }
+ `,
+ },
+ dependencies: {},
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it(`adds the header for a matched path`, async () => {
+ const res1 = await fetchViaHTTP(next.url, `/root`)
+ expect(await res1.text()).toContain(`(en) Hello from /`)
+ expect(res1.headers.get('X-From-Middleware')).toBe('true')
+ const res2 = await fetchViaHTTP(next.url, `/root/es`)
+ expect(await res2.text()).toContain(`(es) Hello from /`)
+ expect(res2.headers.get('X-From-Middleware')).toBe('true')
+ })
+
+ it('adds the header for a mathed root path with /index', async () => {
+ const res1 = await fetchViaHTTP(next.url, `/root/index`)
+ expect(await res1.text()).toContain(`(en) Hello from /`)
+ expect(res1.headers.get('X-From-Middleware')).toBe('true')
+ const res2 = await fetchViaHTTP(next.url, `/root/es/index`)
+ expect(await res2.text()).toContain(`(es) Hello from /`)
+ expect(res2.headers.get('X-From-Middleware')).toBe('true')
+ })
+
+ it(`adds the headers for a matched data path`, async () => {
+ const res1 = await fetchViaHTTP(
+ next.url,
+ `/root/_next/data/${next.buildId}/en.json`,
+ undefined,
+ { headers: { 'x-nextjs-data': '1' } }
+ )
+ expect(await res1.json()).toMatchObject({
+ pageProps: { message: `(en) Hello from /` },
+ })
+ expect(res1.headers.get('X-From-Middleware')).toBe('true')
+ const res2 = await fetchViaHTTP(
+ next.url,
+ `/root/_next/data/${next.buildId}/es.json`,
+ undefined,
+ { headers: { 'x-nextjs-data': '1' } }
+ )
+ expect(await res2.json()).toMatchObject({
+ pageProps: { message: `(es) Hello from /` },
+ })
+ expect(res2.headers.get('X-From-Middleware')).toBe('true')
+ })
+
+ it(`does not add the header for an unmatched path`, async () => {
+ const response = await fetchViaHTTP(next.url, `/root/about/me`)
+ expect(await response.text()).toContain('Hello from /about/me')
+ expect(response.headers.get('X-From-Middleware')).toBeNull()
+ })
+ }
+)
diff --git a/test/e2e/tests/middleware-responses/app/middleware.js b/test/e2e/tests/middleware-responses/app/middleware.js
new file mode 100644
index 0000000000..abc81bbe91
--- /dev/null
+++ b/test/e2e/tests/middleware-responses/app/middleware.js
@@ -0,0 +1,74 @@
+import { NextResponse } from 'next/server'
+
+// we use this trick to fool static analysis at build time, so we can build a
+// middleware that will return a body at run time, and check it is disallowed.
+class MyResponse extends Response {}
+
+export async function middleware(request, ev) {
+ // eslint-disable-next-line no-undef
+ const { readable, writable } = new TransformStream()
+ const url = request.nextUrl
+ const writer = writable.getWriter()
+ const encoder = new TextEncoder()
+ const next = NextResponse.next()
+
+ // this is needed for tests to get the BUILD_ID
+ if (url.pathname.startsWith('/_next/static/__BUILD_ID')) {
+ return NextResponse.next()
+ }
+
+ // Header based on query param
+ if (url.searchParams.get('nested-header') === 'true') {
+ next.headers.set('x-nested-header', 'valid')
+ }
+
+ // Ensure deep can append to this value
+ if (url.searchParams.get('append-me') === 'true') {
+ next.headers.append('x-append-me', 'top')
+ }
+
+ // Ensure deep can append to this value
+ if (url.searchParams.get('cookie-me') === 'true') {
+ next.headers.append('set-cookie', 'bar=chocochip')
+ }
+
+ // Sends a header
+ if (url.pathname === '/header') {
+ next.headers.set('x-first-header', 'valid')
+ return next
+ }
+
+ if (url.pathname === '/two-cookies') {
+ const headers = new Headers()
+ headers.append('set-cookie', 'foo=chocochip')
+ headers.append('set-cookie', 'bar=chocochip')
+ return new Response(null, { headers })
+ }
+
+ // Streams a basic response
+ if (url.pathname === '/stream-a-response') {
+ ev.waitUntil(
+ (async () => {
+ writer.write(encoder.encode('this is a streamed '))
+ writer.write(encoder.encode('response '))
+ writer.close()
+ })()
+ )
+
+ return new MyResponse(readable)
+ }
+
+ if (url.pathname === '/bad-status') {
+ return new Response(null, {
+ headers: { 'WWW-Authenticate': 'Basic realm="Secure Area"' },
+ status: 401,
+ })
+ }
+
+ // Sends response
+ if (url.pathname === '/send-response') {
+ return new MyResponse(JSON.stringify({ message: 'hi!' }))
+ }
+
+ return next
+}
diff --git a/test/e2e/tests/middleware-responses/app/next.config.js b/test/e2e/tests/middleware-responses/app/next.config.js
new file mode 100644
index 0000000000..f548199a3b
--- /dev/null
+++ b/test/e2e/tests/middleware-responses/app/next.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ i18n: {
+ locales: ['en', 'fr', 'nl'],
+ defaultLocale: 'en',
+ },
+}
diff --git a/test/e2e/tests/middleware-responses/app/pages/index.js b/test/e2e/tests/middleware-responses/app/pages/index.js
new file mode 100644
index 0000000000..e931b33f81
--- /dev/null
+++ b/test/e2e/tests/middleware-responses/app/pages/index.js
@@ -0,0 +1,48 @@
+import Link from 'next/link'
+
+export default function Home({ message }) {
+ return (
+
+
Hello {message}
+
Stream a response
+
+
Stream a long response
+
Test streaming after response ends
+
+
+ Attempt to add a header after stream ends
+
+
+
+ Redirect to Google and attempt to stream after
+
+
+
Respond with a header
+
+
+ Respond with 2 headers (nested middleware effect)
+
+
+
Respond with body, end, set a header
+
+
+ Respond with body, end, send another body
+
+
+
Respond with body
+
+
Redirect and then send a body
+
+
Send React component as a body
+
+
Stream React component
+
+
404
+
+
+ )
+}
+
+export const getServerSideProps = ({ query }) => ({
+ props: { message: query.message || 'World' },
+})
diff --git a/test/e2e/tests/middleware-responses/test/index.test.ts b/test/e2e/tests/middleware-responses/test/index.test.ts
new file mode 100644
index 0000000000..6fd4bcd962
--- /dev/null
+++ b/test/e2e/tests/middleware-responses/test/index.test.ts
@@ -0,0 +1,89 @@
+/* eslint-env jest */
+
+import { join } from 'path'
+import { fetchViaHTTP } from 'next-test-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { createNext, FileRef } from 'e2e-utils'
+
+describe('Middleware Responses', () => {
+ let next: NextInstance
+
+ afterAll(() => next.destroy())
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ pages: new FileRef(join(__dirname, '../app/pages')),
+ 'middleware.js': new FileRef(join(__dirname, '../app/middleware.js')),
+ 'next.config.js': new FileRef(join(__dirname, '../app/next.config.js')),
+ },
+ })
+ })
+ function testsWithLocale(locale = '') {
+ const label = locale ? `${locale} ` : ``
+
+ it(`${label}responds with multiple cookies`, async () => {
+ const res = await fetchViaHTTP(next.url, `${locale}/two-cookies`)
+ expect(res.headers.raw()['set-cookie']).toEqual([
+ 'foo=chocochip',
+ 'bar=chocochip',
+ ])
+ })
+
+ it(`${label}should fail when returning a stream`, async () => {
+ const res = await fetchViaHTTP(next.url, `${locale}/stream-a-response`)
+ expect(res.status).toBe(500)
+
+ if (!(global as any).isNextDeploy) {
+ expect(await res.text()).toEqual('Internal Server Error')
+ expect(next.cliOutput).toContain(
+ `A middleware can not alter response's body. Learn more: https://nextjs.org/docs/messages/returning-response-body-in-middleware`
+ )
+ }
+ })
+
+ it(`${label}should fail when returning a text body`, async () => {
+ const res = await fetchViaHTTP(next.url, `${locale}/send-response`)
+ expect(res.status).toBe(500)
+
+ if (!(global as any).isNextDeploy) {
+ expect(await res.text()).toEqual('Internal Server Error')
+ expect(next.cliOutput).toContain(
+ `A middleware can not alter response's body. Learn more: https://nextjs.org/docs/messages/returning-response-body-in-middleware`
+ )
+ }
+ })
+
+ it(`${label}should respond with a 401 status code`, async () => {
+ const res = await fetchViaHTTP(next.url, `${locale}/bad-status`)
+ const html = await res.text()
+ expect(res.status).toBe(401)
+ expect(html).toBe('')
+ })
+
+ it(`${label}should respond with one header`, async () => {
+ const res = await fetchViaHTTP(next.url, `${locale}/header`)
+ expect(res.headers.get('x-first-header')).toBe('valid')
+ })
+
+ it(`${label}should respond with two headers`, async () => {
+ const res = await fetchViaHTTP(
+ next.url,
+ `${locale}/header?nested-header=true`
+ )
+ expect(res.headers.get('x-first-header')).toBe('valid')
+ expect(res.headers.get('x-nested-header')).toBe('valid')
+ })
+
+ it(`${label}should respond appending headers headers`, async () => {
+ const res = await fetchViaHTTP(
+ next.url,
+ `${locale}/?nested-header=true&append-me=true&cookie-me=true`
+ )
+ expect(res.headers.get('x-nested-header')).toBe('valid')
+ expect(res.headers.get('x-append-me')).toBe('top')
+ expect(res.headers.raw()['set-cookie']).toEqual(['bar=chocochip'])
+ })
+ }
+ testsWithLocale()
+ testsWithLocale('/fr')
+})
diff --git a/test/e2e/tests/og-api/app/next.config.js b/test/e2e/tests/og-api/app/next.config.js
new file mode 100644
index 0000000000..b6b4bc39ee
--- /dev/null
+++ b/test/e2e/tests/og-api/app/next.config.js
@@ -0,0 +1,4 @@
+/** @type {import('next').NextConfig} */
+module.exports = {
+ output: 'standalone',
+}
diff --git a/test/e2e/tests/og-api/app/pages/api/og.js b/test/e2e/tests/og-api/app/pages/api/og.js
new file mode 100644
index 0000000000..1afb24a112
--- /dev/null
+++ b/test/e2e/tests/og-api/app/pages/api/og.js
@@ -0,0 +1,26 @@
+// /pages/api/og.jsx
+import { ImageResponse } from '@vercel/og'
+
+export const config = {
+ runtime: 'experimental-edge',
+}
+
+export default function () {
+ return new ImageResponse(
+ (
+
+ Hello!
+
+ )
+ )
+}
diff --git a/test/e2e/tests/og-api/app/pages/index.js b/test/e2e/tests/og-api/app/pages/index.js
new file mode 100644
index 0000000000..ff7159d914
--- /dev/null
+++ b/test/e2e/tests/og-api/app/pages/index.js
@@ -0,0 +1,3 @@
+export default function Page() {
+ return hello world
+}
diff --git a/test/e2e/tests/og-api/index.test.ts b/test/e2e/tests/og-api/index.test.ts
new file mode 100644
index 0000000000..9da3b614ce
--- /dev/null
+++ b/test/e2e/tests/og-api/index.test.ts
@@ -0,0 +1,30 @@
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils'
+import { join } from 'path'
+
+describe('og-api', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: new FileRef(join(__dirname, 'app')),
+ dependencies: {
+ '@vercel/og': 'latest',
+ },
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('should respond from index', async () => {
+ const html = await renderViaHTTP(next.url, '/')
+ expect(html).toContain('hello world')
+ })
+
+ it('should work', async () => {
+ const res = await fetchViaHTTP(next.url, '/api/og')
+ expect(res.status).toBe(200)
+ const body = await res.blob()
+ expect(body.size).toBeGreaterThan(0)
+ })
+})
diff --git a/test/e2e/tests/ssr-react-context/app/context.js b/test/e2e/tests/ssr-react-context/app/context.js
new file mode 100644
index 0000000000..d50a243270
--- /dev/null
+++ b/test/e2e/tests/ssr-react-context/app/context.js
@@ -0,0 +1,5 @@
+import React from 'react'
+
+export const Idk = React.createContext(null)
+
+export const useIdk = () => React.useContext(Idk)
diff --git a/test/e2e/tests/ssr-react-context/app/pages/_app.js b/test/e2e/tests/ssr-react-context/app/pages/_app.js
new file mode 100644
index 0000000000..5863cbabb2
--- /dev/null
+++ b/test/e2e/tests/ssr-react-context/app/pages/_app.js
@@ -0,0 +1,9 @@
+import { Idk } from '../context'
+
+export default function MyApp({ Component, pageProps }) {
+ return (
+
+
+
+ )
+}
diff --git a/test/e2e/tests/ssr-react-context/app/pages/consumer.js b/test/e2e/tests/ssr-react-context/app/pages/consumer.js
new file mode 100644
index 0000000000..ab8100bff2
--- /dev/null
+++ b/test/e2e/tests/ssr-react-context/app/pages/consumer.js
@@ -0,0 +1,17 @@
+import React from 'react'
+
+const NumberContext = React.createContext(0)
+
+export default function page() {
+ return (
+
+
+ {(value) => Value: {value}
}
+
+
+ )
+}
+
+export async function getServerSideProps() {
+ return { props: {} }
+}
diff --git a/test/e2e/tests/ssr-react-context/app/pages/index.js b/test/e2e/tests/ssr-react-context/app/pages/index.js
new file mode 100644
index 0000000000..2c10108fc5
--- /dev/null
+++ b/test/e2e/tests/ssr-react-context/app/pages/index.js
@@ -0,0 +1,16 @@
+import React from 'react'
+import { useIdk } from '../context'
+
+const Page = () => {
+ const idk = useIdk()
+
+ console.log(idk)
+
+ return (
+ <>
+ Value: {idk}
+ >
+ )
+}
+
+export default Page
diff --git a/test/e2e/tests/ssr-react-context/index.test.ts b/test/e2e/tests/ssr-react-context/index.test.ts
new file mode 100644
index 0000000000..503fe7f852
--- /dev/null
+++ b/test/e2e/tests/ssr-react-context/index.test.ts
@@ -0,0 +1,46 @@
+import { join } from 'path'
+import { renderViaHTTP, check } from 'next-test-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { createNext, FileRef } from 'e2e-utils'
+
+describe('React Context', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ pages: new FileRef(join(__dirname, 'app/pages')),
+ 'context.js': new FileRef(join(__dirname, 'app/context.js')),
+ },
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('should render a page with context', async () => {
+ const html = await renderViaHTTP(next.url, '/')
+ expect(html).toMatch(/Value: .*?hello world/)
+ })
+
+ it('should render correctly with context consumer', async () => {
+ const html = await renderViaHTTP(next.url, '/consumer')
+ expect(html).toMatch(/Value: .*?12345/)
+ })
+
+ if ((globalThis as any).isNextDev) {
+ it('should render with context after change', async () => {
+ const aboutAppPagePath = 'pages/_app.js'
+ const originalContent = await next.readFile(aboutAppPagePath)
+ await next.patchFile(
+ aboutAppPagePath,
+ originalContent.replace('hello world', 'new value')
+ )
+
+ try {
+ await check(() => renderViaHTTP(next.url, '/'), /Value: .*?new value/)
+ } finally {
+ await next.patchFile(aboutAppPagePath, originalContent)
+ }
+ await check(() => renderViaHTTP(next.url, '/'), /Value: .*?hello world/)
+ })
+ }
+})
diff --git a/test/e2e/tests/styled-jsx/app/.npmrc b/test/e2e/tests/styled-jsx/app/.npmrc
new file mode 100644
index 0000000000..4c2f52b3be
--- /dev/null
+++ b/test/e2e/tests/styled-jsx/app/.npmrc
@@ -0,0 +1,2 @@
+auto-install-peers=true
+strict-peer-dependencies=false
diff --git a/test/e2e/tests/styled-jsx/app/node_modules_bak/my-comps/button.js b/test/e2e/tests/styled-jsx/app/node_modules_bak/my-comps/button.js
new file mode 100644
index 0000000000..a800c06432
--- /dev/null
+++ b/test/e2e/tests/styled-jsx/app/node_modules_bak/my-comps/button.js
@@ -0,0 +1,31 @@
+Object.defineProperty(exports, '__esModule', { value: true })
+
+function _interopDefault(ex) {
+ return ex && typeof ex === 'object' && 'default' in ex ? ex['default'] : ex
+}
+
+var _JSXStyle = _interopDefault(require('styled-jsx/style'))
+var React = require('react')
+
+function Button() {
+ return React.createElement(
+ 'button',
+ {
+ className: 'jsx-451104437',
+ },
+ 'hello',
+ React.createElement(
+ _JSXStyle,
+ {
+ id: '451104437',
+ },
+ '.jsx-451104437{color:cyan;}\n/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIkJ1dHRvbi50c3giXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBTWtCLEFBRW1CLFVBQ1oiLCJmaWxlIjoiQnV0dG9uLnRzeCIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCAqIGFzIFJlYWN0IGZyb20gJ3JlYWN0JztcblxuZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gQnV0dG9uKCkge1xuICByZXR1cm4gKFxuICAgIDxidXR0b24+XG4gICAgICBoZWxsb1xuICAgICAgPHN0eWxlIGpzeD57YFxuICAgICAgICBjb2xybzogcmVkO1xuICAgICAgYH08L3N0eWxlPlxuICAgIDwvYnV0dG9uPlxuICApO1xufVxuIl19 */\n/*@ sourceURL=Button.tsx */'
+ )
+ )
+}
+
+/**
+ * @class ExampleComponent
+ */
+
+exports.Button = Button
diff --git a/test/e2e/tests/styled-jsx/app/pages/amp.js b/test/e2e/tests/styled-jsx/app/pages/amp.js
new file mode 100644
index 0000000000..8f71fdbc5c
--- /dev/null
+++ b/test/e2e/tests/styled-jsx/app/pages/amp.js
@@ -0,0 +1,12 @@
+import { Button } from 'my-comps/button'
+
+export const config = { amp: true }
+
+export default function page() {
+ return (
+ <>
+ Hello world
+ Click me
+ >
+ )
+}
diff --git a/test/e2e/tests/styled-jsx/app/pages/index.js b/test/e2e/tests/styled-jsx/app/pages/index.js
new file mode 100644
index 0000000000..378661135f
--- /dev/null
+++ b/test/e2e/tests/styled-jsx/app/pages/index.js
@@ -0,0 +1,11 @@
+import { Button } from 'my-comps/button'
+
+export default function Page() {
+ return (
+
+
+
hello world
+
Click me
+
+ )
+}
diff --git a/test/e2e/tests/styled-jsx/index.test.ts b/test/e2e/tests/styled-jsx/index.test.ts
new file mode 100644
index 0000000000..aa22928ece
--- /dev/null
+++ b/test/e2e/tests/styled-jsx/index.test.ts
@@ -0,0 +1,68 @@
+import path from 'path'
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { renderViaHTTP } from 'next-test-utils'
+import webdriver from 'next-webdriver'
+
+const appDir = path.join(__dirname, 'app')
+
+function runTest() {
+ describe(`styled-jsx`, () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ node_modules_bak: new FileRef(path.join(appDir, 'node_modules_bak')),
+ pages: new FileRef(path.join(appDir, 'pages')),
+ '.npmrc': new FileRef(path.join(appDir, '.npmrc')),
+ },
+ packageJson: {
+ scripts: {
+ setup: `cp -r ./node_modules_bak/my-comps ./node_modules;`,
+ build: `yarn setup && next build`,
+ dev: `yarn setup && next dev`,
+ start: 'next start',
+ },
+ },
+ dependencies: {
+ 'styled-jsx': '5.0.0', // styled-jsx on user side
+ },
+ startCommand: 'yarn ' + ((global as any).isNextDev ? 'dev' : 'start'),
+ buildCommand: `yarn build`,
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('should contain styled-jsx styles during SSR', async () => {
+ const html = await renderViaHTTP(next.url, '/')
+ expect(html).toMatch(/color:.*?red/)
+ expect(html).toMatch(/color:.*?cyan/)
+ })
+
+ it('should render styles during CSR', async () => {
+ const browser = await webdriver(next.url, '/')
+ const color = await browser.eval(
+ `getComputedStyle(document.querySelector('button')).color`
+ )
+
+ expect(color).toMatch('0, 255, 255')
+ })
+
+ it('should render styles during CSR (AMP)', async () => {
+ const browser = await webdriver(next.url, '/amp')
+ const color = await browser.eval(
+ `getComputedStyle(document.querySelector('button')).color`
+ )
+
+ expect(color).toMatch('0, 255, 255')
+ })
+
+ it('should render styles during SSR (AMP)', async () => {
+ const html = await renderViaHTTP(next.url, '/amp')
+ expect(html).toMatch(/color:.*?cyan/)
+ })
+ })
+}
+
+runTest()
diff --git a/test/e2e/tests/type-module-interop/index.test.ts b/test/e2e/tests/type-module-interop/index.test.ts
new file mode 100644
index 0000000000..5e1043d7ca
--- /dev/null
+++ b/test/e2e/tests/type-module-interop/index.test.ts
@@ -0,0 +1,113 @@
+import { createNext } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { hasRedbox, renderViaHTTP } from 'next-test-utils'
+import webdriver from 'next-webdriver'
+import cheerio from 'cheerio'
+
+describe('Type module interop', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ 'pages/index.js': `
+ import Link from 'next/link'
+ import Head from 'next/head'
+ import Script from 'next/script'
+ import dynamic from 'next/dynamic'
+ import { useAmp } from 'next/amp'
+
+ const Dynamic = dynamic(() => import('../components/example'))
+
+ export default function Page() {
+ const isAmp = useAmp()
+ return (
+ <>
+
+ This page has a title 🤔
+
+
+
+
+ hello world
+
+ isAmp: {isAmp ? 'yes' : 'false'}
+
+ link to module
+
+ >
+ )
+ }
+ `,
+ 'pages/modules.jsx': `
+ import Link from 'next/link'
+ import Image from 'next/image'
+
+ export default function Modules() {
+ return (
+ <>
+
+ link to home
+
+
+ >
+ )
+ }
+ `,
+ 'components/example.jsx': `
+ export default function Example() {
+ return An example components load via next/dynamic
+ }
+ `,
+ },
+ dependencies: {},
+ })
+
+ // can't modify build output after deploy
+ if (!(global as any).isNextDeploy) {
+ const contents = await next.readFile('package.json')
+ const pkg = JSON.parse(contents)
+ await next.patchFile(
+ 'package.json',
+ JSON.stringify({
+ ...pkg,
+ type: 'module',
+ })
+ )
+ }
+ })
+ afterAll(() => next.destroy())
+
+ it('should render server-side', async () => {
+ const html = await renderViaHTTP(next.url, '/')
+ expect(html).toContain('hello world')
+ // component load via next/dynamic should rendered on the server side
+ expect(html).toContain('An example components load via next/dynamic')
+ // imported next/amp should work on the server side
+ const $ = cheerio.load(html)
+ expect($('#isAmp').text()).toContain('false')
+ })
+
+ it('should render client-side', async () => {
+ const browser = await webdriver(next.url, '/')
+ expect(await hasRedbox(browser)).toBe(false)
+ await browser.close()
+ })
+
+ it('should render server-side with modules', async () => {
+ const html = await renderViaHTTP(next.url, '/modules')
+ const $ = cheerio.load(html)
+ expect($('#link-to-home').text()).toBe('link to home')
+ })
+
+ it('should render client-side with modules', async () => {
+ const browser = await webdriver(next.url, '/modules')
+ expect(await hasRedbox(browser)).toBe(false)
+ await browser.close()
+ })
+})