diff --git a/test/integration/client-navigation/components/hello.jsx b/test/development/pages-dir/client-navigation/fixture/components/hello.jsx
similarity index 100%
rename from test/integration/client-navigation/components/hello.jsx
rename to test/development/pages-dir/client-navigation/fixture/components/hello.jsx
diff --git a/test/integration/client-navigation/components/hello1.js b/test/development/pages-dir/client-navigation/fixture/components/hello1.js
similarity index 100%
rename from test/integration/client-navigation/components/hello1.js
rename to test/development/pages-dir/client-navigation/fixture/components/hello1.js
diff --git a/test/integration/client-navigation/components/world.jsx b/test/development/pages-dir/client-navigation/fixture/components/world.jsx
similarity index 100%
rename from test/integration/client-navigation/components/world.jsx
rename to test/development/pages-dir/client-navigation/fixture/components/world.jsx
diff --git a/test/integration/client-navigation/lib/async-function.js b/test/development/pages-dir/client-navigation/fixture/lib/async-function.js
similarity index 100%
rename from test/integration/client-navigation/lib/async-function.js
rename to test/development/pages-dir/client-navigation/fixture/lib/async-function.js
diff --git a/test/integration/client-navigation/lib/cdm.js b/test/development/pages-dir/client-navigation/fixture/lib/cdm.js
similarity index 100%
rename from test/integration/client-navigation/lib/cdm.js
rename to test/development/pages-dir/client-navigation/fixture/lib/cdm.js
diff --git a/test/integration/client-navigation/lib/colored-blue.js b/test/development/pages-dir/client-navigation/fixture/lib/colored-blue.js
similarity index 100%
rename from test/integration/client-navigation/lib/colored-blue.js
rename to test/development/pages-dir/client-navigation/fixture/lib/colored-blue.js
diff --git a/test/integration/client-navigation/lib/data.json b/test/development/pages-dir/client-navigation/fixture/lib/data.json
similarity index 100%
rename from test/integration/client-navigation/lib/data.json
rename to test/development/pages-dir/client-navigation/fixture/lib/data.json
diff --git a/test/integration/client-navigation/next.config.js b/test/development/pages-dir/client-navigation/fixture/next.config.js
similarity index 100%
rename from test/integration/client-navigation/next.config.js
rename to test/development/pages-dir/client-navigation/fixture/next.config.js
diff --git a/test/integration/client-navigation/pages/_document.js b/test/development/pages-dir/client-navigation/fixture/pages/_document.js
similarity index 100%
rename from test/integration/client-navigation/pages/_document.js
rename to test/development/pages-dir/client-navigation/fixture/pages/_document.js
diff --git a/test/integration/client-navigation/pages/absolute-url.js b/test/development/pages-dir/client-navigation/fixture/pages/absolute-url.js
similarity index 100%
rename from test/integration/client-navigation/pages/absolute-url.js
rename to test/development/pages-dir/client-navigation/fixture/pages/absolute-url.js
diff --git a/test/integration/client-navigation/pages/async-props.js b/test/development/pages-dir/client-navigation/fixture/pages/async-props.js
similarity index 100%
rename from test/integration/client-navigation/pages/async-props.js
rename to test/development/pages-dir/client-navigation/fixture/pages/async-props.js
diff --git a/test/integration/client-navigation/pages/circular-json-error.js b/test/development/pages-dir/client-navigation/fixture/pages/circular-json-error.js
similarity index 100%
rename from test/integration/client-navigation/pages/circular-json-error.js
rename to test/development/pages-dir/client-navigation/fixture/pages/circular-json-error.js
diff --git a/test/integration/client-navigation/pages/custom-encoding.js b/test/development/pages-dir/client-navigation/fixture/pages/custom-encoding.js
similarity index 100%
rename from test/integration/client-navigation/pages/custom-encoding.js
rename to test/development/pages-dir/client-navigation/fixture/pages/custom-encoding.js
diff --git a/test/integration/client-navigation/pages/custom-extension.jsx b/test/development/pages-dir/client-navigation/fixture/pages/custom-extension.jsx
similarity index 100%
rename from test/integration/client-navigation/pages/custom-extension.jsx
rename to test/development/pages-dir/client-navigation/fixture/pages/custom-extension.jsx
diff --git a/test/integration/client-navigation/pages/default-head.js b/test/development/pages-dir/client-navigation/fixture/pages/default-head.js
similarity index 100%
rename from test/integration/client-navigation/pages/default-head.js
rename to test/development/pages-dir/client-navigation/fixture/pages/default-head.js
diff --git a/test/integration/client-navigation/pages/dynamic/[slug]/route.js b/test/development/pages-dir/client-navigation/fixture/pages/dynamic/[slug]/route.js
similarity index 100%
rename from test/integration/client-navigation/pages/dynamic/[slug]/route.js
rename to test/development/pages-dir/client-navigation/fixture/pages/dynamic/[slug]/route.js
diff --git a/test/integration/client-navigation/pages/dynamic/ssr.js b/test/development/pages-dir/client-navigation/fixture/pages/dynamic/ssr.js
similarity index 100%
rename from test/integration/client-navigation/pages/dynamic/ssr.js
rename to test/development/pages-dir/client-navigation/fixture/pages/dynamic/ssr.js
diff --git a/test/integration/client-navigation/pages/empty-get-initial-props.js b/test/development/pages-dir/client-navigation/fixture/pages/empty-get-initial-props.js
similarity index 100%
rename from test/integration/client-navigation/pages/empty-get-initial-props.js
rename to test/development/pages-dir/client-navigation/fixture/pages/empty-get-initial-props.js
diff --git a/test/integration/client-navigation/pages/error-in-the-browser-global-scope.js b/test/development/pages-dir/client-navigation/fixture/pages/error-in-the-browser-global-scope.js
similarity index 100%
rename from test/integration/client-navigation/pages/error-in-the-browser-global-scope.js
rename to test/development/pages-dir/client-navigation/fixture/pages/error-in-the-browser-global-scope.js
diff --git a/test/integration/client-navigation/pages/error-in-the-global-scope.js b/test/development/pages-dir/client-navigation/fixture/pages/error-in-the-global-scope.js
similarity index 100%
rename from test/integration/client-navigation/pages/error-in-the-global-scope.js
rename to test/development/pages-dir/client-navigation/fixture/pages/error-in-the-global-scope.js
diff --git a/test/integration/client-navigation/pages/error-inside-browser-page.js b/test/development/pages-dir/client-navigation/fixture/pages/error-inside-browser-page.js
similarity index 100%
rename from test/integration/client-navigation/pages/error-inside-browser-page.js
rename to test/development/pages-dir/client-navigation/fixture/pages/error-inside-browser-page.js
diff --git a/test/integration/client-navigation/pages/error-inside-page.js b/test/development/pages-dir/client-navigation/fixture/pages/error-inside-page.js
similarity index 100%
rename from test/integration/client-navigation/pages/error-inside-page.js
rename to test/development/pages-dir/client-navigation/fixture/pages/error-inside-page.js
diff --git a/test/integration/client-navigation/pages/exports.js b/test/development/pages-dir/client-navigation/fixture/pages/exports.js
similarity index 100%
rename from test/integration/client-navigation/pages/exports.js
rename to test/development/pages-dir/client-navigation/fixture/pages/exports.js
diff --git a/test/integration/client-navigation/pages/forwardRef-component.js b/test/development/pages-dir/client-navigation/fixture/pages/forwardRef-component.js
similarity index 100%
rename from test/integration/client-navigation/pages/forwardRef-component.js
rename to test/development/pages-dir/client-navigation/fixture/pages/forwardRef-component.js
diff --git a/test/integration/client-navigation/pages/fragment-syntax.js b/test/development/pages-dir/client-navigation/fixture/pages/fragment-syntax.js
similarity index 100%
rename from test/integration/client-navigation/pages/fragment-syntax.js
rename to test/development/pages-dir/client-navigation/fixture/pages/fragment-syntax.js
diff --git a/test/integration/client-navigation/pages/head-duplicate-default-keys.js b/test/development/pages-dir/client-navigation/fixture/pages/head-duplicate-default-keys.js
similarity index 100%
rename from test/integration/client-navigation/pages/head-duplicate-default-keys.js
rename to test/development/pages-dir/client-navigation/fixture/pages/head-duplicate-default-keys.js
diff --git a/test/integration/client-navigation/pages/head-dynamic.js b/test/development/pages-dir/client-navigation/fixture/pages/head-dynamic.js
similarity index 100%
rename from test/integration/client-navigation/pages/head-dynamic.js
rename to test/development/pages-dir/client-navigation/fixture/pages/head-dynamic.js
diff --git a/test/integration/client-navigation/pages/head-priority.js b/test/development/pages-dir/client-navigation/fixture/pages/head-priority.js
similarity index 100%
rename from test/integration/client-navigation/pages/head-priority.js
rename to test/development/pages-dir/client-navigation/fixture/pages/head-priority.js
diff --git a/test/integration/client-navigation/pages/head-with-json-ld-snippet.js b/test/development/pages-dir/client-navigation/fixture/pages/head-with-json-ld-snippet.js
similarity index 100%
rename from test/integration/client-navigation/pages/head-with-json-ld-snippet.js
rename to test/development/pages-dir/client-navigation/fixture/pages/head-with-json-ld-snippet.js
diff --git a/test/integration/client-navigation/pages/head.js b/test/development/pages-dir/client-navigation/fixture/pages/head.js
similarity index 100%
rename from test/integration/client-navigation/pages/head.js
rename to test/development/pages-dir/client-navigation/fixture/pages/head.js
diff --git a/test/integration/client-navigation/pages/index.js b/test/development/pages-dir/client-navigation/fixture/pages/index.js
similarity index 100%
rename from test/integration/client-navigation/pages/index.js
rename to test/development/pages-dir/client-navigation/fixture/pages/index.js
diff --git a/test/integration/client-navigation/pages/instance-get-initial-props.js b/test/development/pages-dir/client-navigation/fixture/pages/instance-get-initial-props.js
similarity index 100%
rename from test/integration/client-navigation/pages/instance-get-initial-props.js
rename to test/development/pages-dir/client-navigation/fixture/pages/instance-get-initial-props.js
diff --git a/test/integration/client-navigation/pages/json.js b/test/development/pages-dir/client-navigation/fixture/pages/json.js
similarity index 100%
rename from test/integration/client-navigation/pages/json.js
rename to test/development/pages-dir/client-navigation/fixture/pages/json.js
diff --git a/test/integration/client-navigation/pages/link-invalid-onclick.js b/test/development/pages-dir/client-navigation/fixture/pages/link-invalid-onclick.js
similarity index 100%
rename from test/integration/client-navigation/pages/link-invalid-onclick.js
rename to test/development/pages-dir/client-navigation/fixture/pages/link-invalid-onclick.js
diff --git a/test/integration/client-navigation/pages/link-no-child.js b/test/development/pages-dir/client-navigation/fixture/pages/link-no-child.js
similarity index 100%
rename from test/integration/client-navigation/pages/link-no-child.js
rename to test/development/pages-dir/client-navigation/fixture/pages/link-no-child.js
diff --git a/test/integration/client-navigation/pages/link-number-child.js b/test/development/pages-dir/client-navigation/fixture/pages/link-number-child.js
similarity index 100%
rename from test/integration/client-navigation/pages/link-number-child.js
rename to test/development/pages-dir/client-navigation/fixture/pages/link-number-child.js
diff --git a/test/integration/client-navigation/pages/link.js b/test/development/pages-dir/client-navigation/fixture/pages/link.js
similarity index 100%
rename from test/integration/client-navigation/pages/link.js
rename to test/development/pages-dir/client-navigation/fixture/pages/link.js
diff --git a/test/integration/client-navigation/pages/memo-component.js b/test/development/pages-dir/client-navigation/fixture/pages/memo-component.js
similarity index 100%
rename from test/integration/client-navigation/pages/memo-component.js
rename to test/development/pages-dir/client-navigation/fixture/pages/memo-component.js
diff --git a/test/integration/client-navigation/pages/nav/about.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/about.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/about.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/about.js
diff --git a/test/integration/client-navigation/pages/nav/as-path-pushstate.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/as-path-pushstate.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/as-path-pushstate.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/as-path-pushstate.js
diff --git a/test/integration/client-navigation/pages/nav/as-path-query.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/as-path-query.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/as-path-query.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/as-path-query.js
diff --git a/test/integration/client-navigation/pages/nav/as-path-using-router.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/as-path-using-router.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/as-path-using-router.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/as-path-using-router.js
diff --git a/test/integration/client-navigation/pages/nav/as-path.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/as-path.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/as-path.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/as-path.js
diff --git a/test/integration/client-navigation/pages/nav/hash-changes-with-state.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/hash-changes-with-state.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/hash-changes-with-state.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/hash-changes-with-state.js
diff --git a/test/integration/client-navigation/pages/nav/hash-changes.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/hash-changes.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/hash-changes.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/hash-changes.js
diff --git a/test/integration/client-navigation/pages/nav/head-1.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/head-1.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/head-1.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/head-1.js
diff --git a/test/integration/client-navigation/pages/nav/head-2.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/head-2.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/head-2.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/head-2.js
diff --git a/test/integration/client-navigation/pages/nav/head-3.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/head-3.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/head-3.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/head-3.js
diff --git a/test/integration/client-navigation/pages/nav/index.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/index.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/index.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/index.js
diff --git a/test/integration/client-navigation/pages/nav/long-page-to-snap-scroll.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/long-page-to-snap-scroll.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/long-page-to-snap-scroll.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/long-page-to-snap-scroll.js
diff --git a/test/integration/client-navigation/pages/nav/on-click.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/on-click.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/on-click.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/on-click.js
diff --git a/test/integration/client-navigation/pages/nav/pass-href-prop.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/pass-href-prop.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/pass-href-prop.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/pass-href-prop.js
diff --git a/test/integration/client-navigation/pages/nav/query-only.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/query-only.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/query-only.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/query-only.js
diff --git a/test/integration/client-navigation/pages/nav/query-params.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/query-params.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/query-params.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/query-params.js
diff --git a/test/integration/client-navigation/pages/nav/querystring.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/querystring.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/querystring.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/querystring.js
diff --git a/test/integration/client-navigation/pages/nav/redirect.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/redirect.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/redirect.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/redirect.js
diff --git a/test/integration/client-navigation/pages/nav/relative-1.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/relative-1.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/relative-1.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/relative-1.js
diff --git a/test/integration/client-navigation/pages/nav/relative-2.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/relative-2.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/relative-2.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/relative-2.js
diff --git a/test/integration/client-navigation/pages/nav/relative/index.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/relative/index.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/relative/index.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/relative/index.js
diff --git a/test/integration/client-navigation/pages/nav/self-reload.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/self-reload.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/self-reload.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/self-reload.js
diff --git a/test/integration/client-navigation/pages/nav/shallow-routing.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/shallow-routing.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/shallow-routing.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/shallow-routing.js
diff --git a/test/integration/client-navigation/pages/nav/with-hoc.js b/test/development/pages-dir/client-navigation/fixture/pages/nav/with-hoc.js
similarity index 100%
rename from test/integration/client-navigation/pages/nav/with-hoc.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nav/with-hoc.js
diff --git a/test/integration/client-navigation/pages/nested-cdm/index.js b/test/development/pages-dir/client-navigation/fixture/pages/nested-cdm/index.js
similarity index 100%
rename from test/integration/client-navigation/pages/nested-cdm/index.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nested-cdm/index.js
diff --git a/test/integration/client-navigation/pages/nested-index/index/index.js b/test/development/pages-dir/client-navigation/fixture/pages/nested-index/index/index.js
similarity index 100%
rename from test/integration/client-navigation/pages/nested-index/index/index.js
rename to test/development/pages-dir/client-navigation/fixture/pages/nested-index/index/index.js
diff --git a/test/integration/client-navigation/pages/no-default-export.js b/test/development/pages-dir/client-navigation/fixture/pages/no-default-export.js
similarity index 100%
rename from test/integration/client-navigation/pages/no-default-export.js
rename to test/development/pages-dir/client-navigation/fixture/pages/no-default-export.js
diff --git a/test/integration/client-navigation/pages/query.js b/test/development/pages-dir/client-navigation/fixture/pages/query.js
similarity index 100%
rename from test/integration/client-navigation/pages/query.js
rename to test/development/pages-dir/client-navigation/fixture/pages/query.js
diff --git a/test/integration/client-navigation/pages/read-only-object-error.js b/test/development/pages-dir/client-navigation/fixture/pages/read-only-object-error.js
similarity index 100%
rename from test/integration/client-navigation/pages/read-only-object-error.js
rename to test/development/pages-dir/client-navigation/fixture/pages/read-only-object-error.js
diff --git a/test/integration/client-navigation/pages/snap-scroll-position.js b/test/development/pages-dir/client-navigation/fixture/pages/snap-scroll-position.js
similarity index 100%
rename from test/integration/client-navigation/pages/snap-scroll-position.js
rename to test/development/pages-dir/client-navigation/fixture/pages/snap-scroll-position.js
diff --git a/test/integration/client-navigation/pages/stateless.js b/test/development/pages-dir/client-navigation/fixture/pages/stateless.js
similarity index 100%
rename from test/integration/client-navigation/pages/stateless.js
rename to test/development/pages-dir/client-navigation/fixture/pages/stateless.js
diff --git a/test/integration/client-navigation/pages/styled-jsx-external.js b/test/development/pages-dir/client-navigation/fixture/pages/styled-jsx-external.js
similarity index 100%
rename from test/integration/client-navigation/pages/styled-jsx-external.js
rename to test/development/pages-dir/client-navigation/fixture/pages/styled-jsx-external.js
diff --git a/test/integration/client-navigation/pages/styled-jsx.js b/test/development/pages-dir/client-navigation/fixture/pages/styled-jsx.js
similarity index 100%
rename from test/integration/client-navigation/pages/styled-jsx.js
rename to test/development/pages-dir/client-navigation/fixture/pages/styled-jsx.js
diff --git a/test/integration/client-navigation/pages/throw-undefined.js b/test/development/pages-dir/client-navigation/fixture/pages/throw-undefined.js
similarity index 100%
rename from test/integration/client-navigation/pages/throw-undefined.js
rename to test/development/pages-dir/client-navigation/fixture/pages/throw-undefined.js
diff --git a/test/integration/client-navigation/pages/with-cdm.js b/test/development/pages-dir/client-navigation/fixture/pages/with-cdm.js
similarity index 100%
rename from test/integration/client-navigation/pages/with-cdm.js
rename to test/development/pages-dir/client-navigation/fixture/pages/with-cdm.js
diff --git a/test/integration/client-navigation/public/test-async.js b/test/development/pages-dir/client-navigation/fixture/public/test-async.js
similarity index 100%
rename from test/integration/client-navigation/public/test-async.js
rename to test/development/pages-dir/client-navigation/fixture/public/test-async.js
diff --git a/test/integration/client-navigation/public/test-defer.js b/test/development/pages-dir/client-navigation/fixture/public/test-defer.js
similarity index 100%
rename from test/integration/client-navigation/public/test-defer.js
rename to test/development/pages-dir/client-navigation/fixture/public/test-defer.js
diff --git a/test/development/pages-dir/client-navigation/index.test.ts b/test/development/pages-dir/client-navigation/index.test.ts
new file mode 100644
index 0000000000000..63544dbfc2539
--- /dev/null
+++ b/test/development/pages-dir/client-navigation/index.test.ts
@@ -0,0 +1,1810 @@
+/* eslint-env jest */
+
+import {
+ fetchViaHTTP,
+ getRedboxSource,
+ hasRedbox,
+ getRedboxHeader,
+ renderViaHTTP,
+ waitFor,
+ check,
+} from 'next-test-utils'
+import webdriver from 'next-webdriver'
+import path from 'path'
+import renderingSuite from './rendering'
+import { createNextDescribe } from 'e2e-utils'
+
+createNextDescribe(
+ 'Client Navigation',
+ {
+ files: path.join(__dirname, 'fixture'),
+ },
+ ({ next }) => {
+ it('should not reload when visiting /_error directly', async () => {
+ const { status } = await fetchViaHTTP(next.appPort, '/_error')
+ const browser = await webdriver(next.appPort, '/_error')
+
+ await browser.eval('window.hello = true')
+
+ // wait on-demand-entries timeout since it can trigger
+ // reloading non-stop
+ for (let i = 0; i < 15; i++) {
+ expect(await browser.eval('window.hello')).toBe(true)
+ await waitFor(1000)
+ }
+ const html = await browser.eval('document.documentElement.innerHTML')
+
+ expect(status).toBe(404)
+ expect(html).toContain('This page could not be found')
+ expect(html).toContain('404')
+ })
+
+ describe('with ', () => {
+ it('should navigate the page', async () => {
+ const browser = await webdriver(next.appPort, '/nav')
+ const text = await browser
+ .elementByCss('#about-link')
+ .click()
+ .waitForElementByCss('.nav-about')
+ .elementByCss('p')
+ .text()
+
+ expect(text).toBe('This is the about page.')
+ await browser.close()
+ })
+
+ it('should have proper error when no children are provided', async () => {
+ const browser = await webdriver(next.appPort, '/link-no-child')
+ expect(await hasRedbox(browser, true)).toBe(true)
+ expect(await getRedboxHeader(browser)).toContain(
+ 'No children were passed to with `href` of `/about` but one child is required'
+ )
+ })
+
+ it('should not throw error when one number type child is provided', async () => {
+ const browser = await webdriver(next.appPort, '/link-number-child')
+ expect(await hasRedbox(browser, false)).toBe(false)
+ if (browser) await browser.close()
+ })
+
+ it('should navigate back after reload', async () => {
+ const browser = await webdriver(next.appPort, '/nav')
+ await browser.elementByCss('#about-link').click()
+ await browser.waitForElementByCss('.nav-about')
+ await browser.refresh()
+ await waitFor(3000)
+ await browser.back()
+ await waitFor(3000)
+ const text = await browser.elementByCss('#about-link').text()
+ if (browser) await browser.close()
+ expect(text).toMatch(/About/)
+ })
+
+ it('should navigate forwards after reload', async () => {
+ const browser = await webdriver(next.appPort, '/nav')
+ await browser.elementByCss('#about-link').click()
+ await browser.waitForElementByCss('.nav-about')
+ await browser.back()
+ await browser.refresh()
+ await waitFor(3000)
+ await browser.forward()
+ await waitFor(3000)
+ const text = await browser.elementByCss('p').text()
+ if (browser) await browser.close()
+ expect(text).toMatch(/this is the about page/i)
+ })
+
+ it('should error when calling onClick without event', async () => {
+ const browser = await webdriver(next.appPort, '/link-invalid-onclick')
+ expect(await browser.elementByCss('#errors').text()).toBe('0')
+ await browser.elementByCss('#custom-button').click()
+ expect(await browser.elementByCss('#errors').text()).toBe('1')
+ })
+
+ it('should navigate via the client side', async () => {
+ const browser = await webdriver(next.appPort, '/nav')
+
+ const counterText = await browser
+ .elementByCss('#increase')
+ .click()
+ .elementByCss('#about-link')
+ .click()
+ .waitForElementByCss('.nav-about')
+ .elementByCss('#home-link')
+ .click()
+ .waitForElementByCss('.nav-home')
+ .elementByCss('#counter')
+ .text()
+
+ expect(counterText).toBe('Counter: 1')
+ await browser.close()
+ })
+
+ it('should navigate an absolute url', async () => {
+ const browser = await webdriver(
+ next.appPort,
+ `/absolute-url?port=${next.appPort}`
+ )
+ await browser.waitForElementByCss('#absolute-link').click()
+ await check(
+ () => browser.eval(() => window.location.origin),
+ 'https://vercel.com'
+ )
+ })
+
+ it('should call mouse handlers with an absolute url', async () => {
+ const browser = await webdriver(
+ next.appPort,
+ `/absolute-url?port=${next.appPort}`
+ )
+
+ await browser.elementByCss('#absolute-link-mouse-events').moveTo()
+
+ expect(
+ await browser
+ .waitForElementByCss('#absolute-link-mouse-events')
+ .getAttribute('data-hover')
+ ).toBe('true')
+ })
+
+ it('should navigate an absolute local url', async () => {
+ const browser = await webdriver(
+ next.appPort,
+ `/absolute-url?port=${next.appPort}`
+ )
+ // @ts-expect-error _didNotNavigate is set intentionally
+ await browser.eval(() => (window._didNotNavigate = true))
+ await browser.waitForElementByCss('#absolute-local-link').click()
+ const text = await browser
+ .waitForElementByCss('.nav-about')
+ .elementByCss('p')
+ .text()
+
+ expect(text).toBe('This is the about page.')
+ // @ts-expect-error _didNotNavigate is set intentionally
+ expect(await browser.eval(() => window._didNotNavigate)).toBe(true)
+ })
+
+ it('should navigate an absolute local url with as', async () => {
+ const browser = await webdriver(
+ next.appPort,
+ `/absolute-url?port=${next.appPort}`
+ )
+ // @ts-expect-error _didNotNavigate is set intentionally
+ await browser.eval(() => (window._didNotNavigate = true))
+ await browser
+ .waitForElementByCss('#absolute-local-dynamic-link')
+ .click()
+ expect(await browser.waitForElementByCss('#dynamic-page').text()).toBe(
+ 'hello'
+ )
+ // @ts-expect-error _didNotNavigate is set intentionally
+ expect(await browser.eval(() => window._didNotNavigate)).toBe(true)
+ })
+ })
+
+ describe('with tag inside the ', () => {
+ it('should navigate the page', async () => {
+ const browser = await webdriver(next.appPort, '/nav/about')
+ const text = await browser
+ .elementByCss('#home-link')
+ .click()
+ .waitForElementByCss('.nav-home')
+ .elementByCss('p')
+ .text()
+
+ expect(text).toBe('This is the home.')
+ await browser.close()
+ })
+
+ it('should not navigate if the tag has a target', async () => {
+ const browser = await webdriver(next.appPort, '/nav')
+
+ await browser
+ .elementByCss('#increase')
+ .click()
+ .elementByCss('#target-link')
+ .click()
+
+ await waitFor(1000)
+
+ const counterText = await browser.elementByCss('#counter').text()
+
+ expect(counterText).toBe('Counter: 1')
+ await browser.close()
+ })
+
+ it('should not navigate if the click-event is modified', async () => {
+ const browser = await webdriver(next.appPort, '/nav')
+
+ await browser.elementByCss('#increase').click()
+
+ const key = process.platform === 'darwin' ? 'Meta' : 'Control'
+
+ await browser.keydown(key)
+
+ await browser.elementByCss('#in-svg-link').click()
+
+ await browser.keyup(key)
+ await waitFor(1000)
+
+ const counterText = await browser.elementByCss('#counter').text()
+
+ expect(counterText).toBe('Counter: 1')
+ await browser.close()
+ })
+
+ it('should not reload when link in svg is clicked', async () => {
+ const browser = await webdriver(next.appPort, '/nav')
+ await browser.eval('window.hello = true')
+ await browser
+ .elementByCss('#in-svg-link')
+ .click()
+ .waitForElementByCss('.nav-about')
+
+ expect(await browser.eval('window.hello')).toBe(true)
+ await browser.close()
+ })
+ })
+
+ describe('with unexpected nested tag', () => {
+ it('should not redirect if passHref prop is not defined in Link', async () => {
+ const browser = await webdriver(next.appPort, '/nav/pass-href-prop')
+ const text = await browser
+ .elementByCss('#without-href')
+ .click()
+ .waitForElementByCss('.nav-pass-href-prop')
+ .elementByCss('p')
+ .text()
+
+ expect(text).toBe('This is the passHref prop page.')
+ await browser.close()
+ })
+
+ it('should redirect if passHref prop is defined in Link', async () => {
+ const browser = await webdriver(next.appPort, '/nav/pass-href-prop')
+ const text = await browser
+ .elementByCss('#with-href')
+ .click()
+ .waitForElementByCss('.nav-home')
+ .elementByCss('p')
+ .text()
+
+ expect(text).toBe('This is the home.')
+ await browser.close()
+ })
+ })
+
+ describe('with empty getInitialProps()', () => {
+ it('should render an error', async () => {
+ let browser
+ try {
+ browser = await webdriver(next.appPort, '/nav')
+ await browser.elementByCss('#empty-props').click()
+ expect(await hasRedbox(browser, true)).toBe(true)
+ expect(await getRedboxHeader(browser)).toMatch(
+ /should resolve to an object\. But found "null" instead\./
+ )
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+ })
+
+ describe('with the same page but different querystring', () => {
+ it('should navigate the page', async () => {
+ const browser = await webdriver(next.appPort, '/nav/querystring?id=1')
+ const text = await browser
+ .elementByCss('#next-id-link')
+ .click()
+ .waitForElementByCss('.nav-id-2')
+ .elementByCss('p')
+ .text()
+
+ expect(text).toBe('2')
+ await browser.close()
+ })
+
+ it('should remove querystring', async () => {
+ const browser = await webdriver(next.appPort, '/nav/querystring?id=1')
+ const text = await browser
+ .elementByCss('#main-page')
+ .click()
+ .waitForElementByCss('.nav-id-0')
+ .elementByCss('p')
+ .text()
+
+ expect(text).toBe('0')
+ await browser.close()
+ })
+ })
+
+ describe('with the current url', () => {
+ it('should reload the page', async () => {
+ const browser = await webdriver(next.appPort, '/nav/self-reload')
+ const defaultCount = await browser.elementByCss('p').text()
+ expect(defaultCount).toBe('COUNT: 0')
+
+ const countAfterClicked = await browser
+ .elementByCss('#self-reload-link')
+ .click()
+ .elementByCss('p')
+ .text()
+
+ expect(countAfterClicked).toBe('COUNT: 1')
+ await browser.close()
+ })
+
+ it('should always replace the state', async () => {
+ const browser = await webdriver(next.appPort, '/nav')
+
+ const countAfterClicked = await browser
+ .elementByCss('#self-reload-link')
+ .click()
+ .waitForElementByCss('#self-reload-page')
+ .elementByCss('#self-reload-link')
+ .click()
+ .elementByCss('#self-reload-link')
+ .click()
+ .elementByCss('p')
+ .text()
+
+ // counts (page change + two clicks)
+ expect(countAfterClicked).toBe('COUNT: 3')
+
+ // Since we replace the state, back button would simply go us back to /nav
+ await browser.back().waitForElementByCss('.nav-home')
+
+ await browser.close()
+ })
+ })
+
+ describe('with onClick action', () => {
+ it('should reload the page and perform additional action', async () => {
+ let browser
+ try {
+ browser = await webdriver(next.appPort, '/nav/on-click')
+ const defaultCountQuery = await browser
+ .elementByCss('#query-count')
+ .text()
+ const defaultCountState = await browser
+ .elementByCss('#state-count')
+ .text()
+ expect(defaultCountQuery).toBe('QUERY COUNT: 0')
+ expect(defaultCountState).toBe('STATE COUNT: 0')
+
+ await browser.elementByCss('#on-click-link').click()
+
+ const countQueryAfterClicked = await browser
+ .elementByCss('#query-count')
+ .text()
+ const countStateAfterClicked = await browser
+ .elementByCss('#state-count')
+ .text()
+ expect(countQueryAfterClicked).toBe('QUERY COUNT: 1')
+ expect(countStateAfterClicked).toBe('STATE COUNT: 1')
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should not reload if default was prevented', async () => {
+ let browser
+ try {
+ browser = await webdriver(next.appPort, '/nav/on-click')
+ const defaultCountQuery = await browser
+ .elementByCss('#query-count')
+ .text()
+ const defaultCountState = await browser
+ .elementByCss('#state-count')
+ .text()
+ expect(defaultCountQuery).toBe('QUERY COUNT: 0')
+ expect(defaultCountState).toBe('STATE COUNT: 0')
+
+ await browser.elementByCss('#on-click-link-prevent-default').click()
+
+ const countQueryAfterClicked = await browser
+ .elementByCss('#query-count')
+ .text()
+ const countStateAfterClicked = await browser
+ .elementByCss('#state-count')
+ .text()
+ expect(countQueryAfterClicked).toBe('QUERY COUNT: 0')
+ expect(countStateAfterClicked).toBe('STATE COUNT: 1')
+
+ await browser.elementByCss('#on-click-link').click()
+
+ const countQueryAfterClickedAgain = await browser
+ .elementByCss('#query-count')
+ .text()
+ const countStateAfterClickedAgain = await browser
+ .elementByCss('#state-count')
+ .text()
+ expect(countQueryAfterClickedAgain).toBe('QUERY COUNT: 1')
+ expect(countStateAfterClickedAgain).toBe('STATE COUNT: 2')
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should always replace the state and perform additional action', async () => {
+ let browser
+ try {
+ browser = await webdriver(next.appPort, '/nav')
+
+ await browser
+ .elementByCss('#on-click-link')
+ .click()
+ .waitForElementByCss('#on-click-page')
+
+ const defaultCountQuery = await browser
+ .elementByCss('#query-count')
+ .text()
+ expect(defaultCountQuery).toBe('QUERY COUNT: 1')
+
+ await browser.elementByCss('#on-click-link').click()
+ const countQueryAfterClicked = await browser
+ .elementByCss('#query-count')
+ .text()
+ const countStateAfterClicked = await browser
+ .elementByCss('#state-count')
+ .text()
+ expect(countQueryAfterClicked).toBe('QUERY COUNT: 2')
+ expect(countStateAfterClicked).toBe('STATE COUNT: 1')
+
+ // Since we replace the state, back button would simply go us back to /nav
+ await browser.back().waitForElementByCss('.nav-home')
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+ })
+ describe('resets scroll at the correct time', () => {
+ it('should reset scroll before the new page runs its lifecycles ()', async () => {
+ let browser
+ try {
+ browser = await webdriver(
+ next.appPort,
+ '/nav/long-page-to-snap-scroll'
+ )
+
+ // Scrolls to item 400 on the page
+ await browser
+ .waitForElementByCss('#long-page-to-snap-scroll')
+ .elementByCss('#scroll-to-item-400')
+ .click()
+
+ const scrollPosition = await browser.eval('window.pageYOffset')
+ expect(scrollPosition).toBe(7208)
+
+ // Go to snap scroll page
+ await browser
+ .elementByCss('#goto-snap-scroll-position')
+ .click()
+ .waitForElementByCss('#scroll-pos-y')
+
+ const snappedScrollPosition = await browser.eval(
+ 'document.getElementById("scroll-pos-y").innerText'
+ )
+ expect(snappedScrollPosition).toBe('0')
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should reset scroll before the new page runs its lifecycles (Router#push)', async () => {
+ let browser
+ try {
+ browser = await webdriver(
+ next.appPort,
+ '/nav/long-page-to-snap-scroll'
+ )
+
+ // Scrolls to item 400 on the page
+ await browser
+ .waitForElementByCss('#long-page-to-snap-scroll')
+ .elementByCss('#scroll-to-item-400')
+ .click()
+
+ const scrollPosition = await browser.eval('window.pageYOffset')
+ expect(scrollPosition).toBe(7208)
+
+ // Go to snap scroll page
+ await browser
+ .elementByCss('#goto-snap-scroll-position-imperative')
+ .click()
+ .waitForElementByCss('#scroll-pos-y')
+
+ const snappedScrollPosition = await browser.eval(
+ 'document.getElementById("scroll-pos-y").innerText'
+ )
+ expect(snappedScrollPosition).toBe('0')
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should intentionally not reset scroll before the new page runs its lifecycles (Router#push)', async () => {
+ let browser
+ try {
+ browser = await webdriver(
+ next.appPort,
+ '/nav/long-page-to-snap-scroll'
+ )
+
+ // Scrolls to item 400 on the page
+ await browser
+ .waitForElementByCss('#long-page-to-snap-scroll')
+ .elementByCss('#scroll-to-item-400')
+ .click()
+
+ const scrollPosition = await browser.eval('window.pageYOffset')
+ expect(scrollPosition).toBe(7208)
+
+ // Go to snap scroll page
+ await browser
+ .elementByCss('#goto-snap-scroll-position-imperative-noscroll')
+ .click()
+ .waitForElementByCss('#scroll-pos-y')
+
+ const snappedScrollPosition = await browser.eval(
+ 'document.getElementById("scroll-pos-y").innerText'
+ )
+ expect(snappedScrollPosition).not.toBe('0')
+ expect(Number(snappedScrollPosition)).toBeGreaterThanOrEqual(7208)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+ })
+
+ describe('with hash changes', () => {
+ describe('check hydration mis-match', () => {
+ it('should not have hydration mis-match for hash link', async () => {
+ const browser = await webdriver(next.appPort, '/nav/hash-changes')
+ const browserLogs = await browser.log('browser')
+ let found = false
+ browserLogs.forEach((log) => {
+ console.log('log.message', log.message)
+ if (log.message.includes('Warning: Prop')) {
+ found = true
+ }
+ })
+ expect(found).toEqual(false)
+ })
+ })
+
+ describe('when hash change via Link', () => {
+ it('should not run getInitialProps', async () => {
+ const browser = await webdriver(next.appPort, '/nav/hash-changes')
+
+ const counter = await browser
+ .elementByCss('#via-link')
+ .click()
+ .elementByCss('p')
+ .text()
+
+ expect(counter).toBe('COUNT: 0')
+
+ await browser.close()
+ })
+
+ it('should scroll to the specified position on the same page', async () => {
+ let browser
+ try {
+ browser = await webdriver(next.appPort, '/nav/hash-changes')
+
+ // Scrolls to item 400 on the page
+ await browser.elementByCss('#scroll-to-item-400').click()
+
+ const scrollPositionBeforeEmptyHash = await browser.eval(
+ 'window.pageYOffset'
+ )
+
+ expect(scrollPositionBeforeEmptyHash).toBe(7258)
+
+ // Scrolls back to top when scrolling to `#` with no value.
+ await browser.elementByCss('#via-empty-hash').click()
+
+ const scrollPositionAfterEmptyHash = await browser.eval(
+ 'window.pageYOffset'
+ )
+
+ expect(scrollPositionAfterEmptyHash).toBe(0)
+
+ // Scrolls to item 400 on the page
+ await browser.elementByCss('#scroll-to-item-400').click()
+
+ const scrollPositionBeforeTopHash = await browser.eval(
+ 'window.pageYOffset'
+ )
+
+ expect(scrollPositionBeforeTopHash).toBe(7258)
+
+ // Scrolls back to top when clicking link with href `#top`.
+ await browser.elementByCss('#via-top-hash').click()
+
+ const scrollPositionAfterTopHash = await browser.eval(
+ 'window.pageYOffset'
+ )
+
+ expect(scrollPositionAfterTopHash).toBe(0)
+
+ // Scrolls to cjk anchor on the page
+ await browser.elementByCss('#scroll-to-cjk-anchor').click()
+
+ const scrollPositionCJKHash = await browser.eval(
+ 'window.pageYOffset'
+ )
+
+ expect(scrollPositionCJKHash).toBe(17436)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should not scroll to hash when scroll={false} is set', async () => {
+ const browser = await webdriver(next.appPort, '/nav/hash-changes')
+ const curScroll = await browser.eval(
+ 'document.documentElement.scrollTop'
+ )
+ await browser
+ .elementByCss('#scroll-to-name-item-400-no-scroll')
+ .click()
+ expect(curScroll).toBe(
+ await browser.eval('document.documentElement.scrollTop')
+ )
+ })
+
+ it('should scroll to the specified position on the same page with a name property', async () => {
+ let browser
+ try {
+ browser = await webdriver(next.appPort, '/nav/hash-changes')
+
+ // Scrolls to item 400 with name="name-item-400" on the page
+ await browser.elementByCss('#scroll-to-name-item-400').click()
+
+ const scrollPosition = await browser.eval('window.pageYOffset')
+
+ expect(scrollPosition).toBe(16258)
+
+ // Scrolls back to top when scrolling to `#` with no value.
+ await browser.elementByCss('#via-empty-hash').click()
+
+ const scrollPositionAfterEmptyHash = await browser.eval(
+ 'window.pageYOffset'
+ )
+
+ expect(scrollPositionAfterEmptyHash).toBe(0)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should scroll to the specified position to a new page', async () => {
+ let browser
+ try {
+ browser = await webdriver(next.appPort, '/nav')
+
+ // Scrolls to item 400 on the page
+ await browser
+ .elementByCss('#scroll-to-hash')
+ .click()
+ .waitForElementByCss('#hash-changes-page')
+
+ const scrollPosition = await browser.eval('window.pageYOffset')
+ expect(scrollPosition).toBe(7258)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should scroll to the specified CJK position to a new page', async () => {
+ let browser
+ try {
+ browser = await webdriver(next.appPort, '/nav')
+
+ // Scrolls to CJK anchor on the page
+ await browser
+ .elementByCss('#scroll-to-cjk-hash')
+ .click()
+ .waitForElementByCss('#hash-changes-page')
+
+ const scrollPosition = await browser.eval('window.pageYOffset')
+ expect(scrollPosition).toBe(17436)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('Should update asPath', async () => {
+ let browser
+ try {
+ browser = await webdriver(next.appPort, '/nav/hash-changes')
+
+ await browser.elementByCss('#via-link').click()
+
+ const asPath = await browser.elementByCss('div#asPath').text()
+ expect(asPath).toBe('ASPATH: /nav/hash-changes#via-link')
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+ })
+
+ describe('when hash change via A tag', () => {
+ it('should not run getInitialProps', async () => {
+ const browser = await webdriver(next.appPort, '/nav/hash-changes')
+
+ const counter = await browser
+ .elementByCss('#via-a')
+ .click()
+ .elementByCss('p')
+ .text()
+
+ expect(counter).toBe('COUNT: 0')
+
+ await browser.close()
+ })
+ })
+
+ describe('when hash get removed', () => {
+ it('should not run getInitialProps', async () => {
+ const browser = await webdriver(next.appPort, '/nav/hash-changes')
+
+ const counter = await browser
+ .elementByCss('#via-a')
+ .click()
+ .elementByCss('#page-url')
+ .click()
+ .elementByCss('p')
+ .text()
+
+ expect(counter).toBe('COUNT: 1')
+
+ await browser.close()
+ })
+
+ it('should not run getInitialProps when removing via back', async () => {
+ const browser = await webdriver(next.appPort, '/nav/hash-changes')
+
+ const counter = await browser
+ .elementByCss('#scroll-to-item-400')
+ .click()
+ .back()
+ .elementByCss('p')
+ .text()
+
+ expect(counter).toBe('COUNT: 0')
+ await browser.close()
+ })
+ })
+
+ describe('when hash set to empty', () => {
+ it('should not run getInitialProps', async () => {
+ const browser = await webdriver(next.appPort, '/nav/hash-changes')
+
+ const counter = await browser
+ .elementByCss('#via-a')
+ .click()
+ .elementByCss('#via-empty-hash')
+ .click()
+ .elementByCss('p')
+ .text()
+
+ expect(counter).toBe('COUNT: 0')
+
+ await browser.close()
+ })
+ })
+ })
+
+ describe('with hash changes with state', () => {
+ describe('when passing state via hash change', () => {
+ it('should increment the history state counter', async () => {
+ const browser = await webdriver(
+ next.appPort,
+ '/nav/hash-changes-with-state#'
+ )
+
+ const historyCount = await browser
+ .elementByCss('#increment-history-count')
+ .click()
+ .elementByCss('#increment-history-count')
+ .click()
+ .elementByCss('div#history-count')
+ .text()
+
+ expect(historyCount).toBe('HISTORY COUNT: 2')
+
+ const counter = await browser.elementByCss('p').text()
+
+ // getInitialProps should not be called with only hash changes
+ expect(counter).toBe('COUNT: 0')
+
+ await browser.close()
+ })
+
+ it('should increment the shallow history state counter', async () => {
+ const browser = await webdriver(
+ next.appPort,
+ '/nav/hash-changes-with-state#'
+ )
+
+ const historyCount = await browser
+ .elementByCss('#increment-shallow-history-count')
+ .click()
+ .elementByCss('#increment-shallow-history-count')
+ .click()
+ .elementByCss('div#shallow-history-count')
+ .text()
+
+ expect(historyCount).toBe('SHALLOW HISTORY COUNT: 2')
+
+ const counter = await browser.elementByCss('p').text()
+
+ expect(counter).toBe('COUNT: 0')
+
+ await browser.close()
+ })
+ })
+ })
+
+ describe('with shallow routing', () => {
+ it('should update the url without running getInitialProps', async () => {
+ const browser = await webdriver(next.appPort, '/nav/shallow-routing')
+ const counter = await browser
+ .elementByCss('#increase')
+ .click()
+ .elementByCss('#increase')
+ .click()
+ .elementByCss('#counter')
+ .text()
+ expect(counter).toBe('Counter: 2')
+
+ const getInitialPropsRunCount = await browser
+ .elementByCss('#get-initial-props-run-count')
+ .text()
+ expect(getInitialPropsRunCount).toBe('getInitialProps run count: 1')
+
+ await browser.close()
+ })
+
+ it('should handle the back button and should not run getInitialProps', async () => {
+ const browser = await webdriver(next.appPort, '/nav/shallow-routing')
+ let counter = await browser
+ .elementByCss('#increase')
+ .click()
+ .elementByCss('#increase')
+ .click()
+ .elementByCss('#counter')
+ .text()
+ expect(counter).toBe('Counter: 2')
+
+ counter = await browser.back().elementByCss('#counter').text()
+ expect(counter).toBe('Counter: 1')
+
+ const getInitialPropsRunCount = await browser
+ .elementByCss('#get-initial-props-run-count')
+ .text()
+ expect(getInitialPropsRunCount).toBe('getInitialProps run count: 1')
+
+ await browser.close()
+ })
+
+ it('should run getInitialProps always when rending the page to the screen', async () => {
+ const browser = await webdriver(next.appPort, '/nav/shallow-routing')
+
+ const counter = await browser
+ .elementByCss('#increase')
+ .click()
+ .elementByCss('#increase')
+ .click()
+ .elementByCss('#home-link')
+ .click()
+ .waitForElementByCss('.nav-home')
+ .back()
+ .waitForElementByCss('.shallow-routing')
+ .elementByCss('#counter')
+ .text()
+ expect(counter).toBe('Counter: 2')
+
+ const getInitialPropsRunCount = await browser
+ .elementByCss('#get-initial-props-run-count')
+ .text()
+ expect(getInitialPropsRunCount).toBe('getInitialProps run count: 2')
+
+ await browser.close()
+ })
+
+ it('should keep the scroll position on shallow routing', async () => {
+ const browser = await webdriver(next.appPort, '/nav/shallow-routing')
+ await browser.eval(() =>
+ document.querySelector('#increase').scrollIntoView()
+ )
+ const scrollPosition = await browser.eval('window.pageYOffset')
+
+ expect(scrollPosition).toBeGreaterThan(3000)
+
+ await browser.elementByCss('#increase').click()
+ await waitFor(500)
+ const newScrollPosition = await browser.eval('window.pageYOffset')
+
+ expect(newScrollPosition).toBe(scrollPosition)
+
+ await browser.elementByCss('#increase2').click()
+ await waitFor(500)
+ const newScrollPosition2 = await browser.eval('window.pageYOffset')
+
+ expect(newScrollPosition2).toBe(0)
+
+ await browser.eval(() =>
+ document.querySelector('#invalidShallow').scrollIntoView()
+ )
+ const scrollPositionDown = await browser.eval('window.pageYOffset')
+
+ expect(scrollPositionDown).toBeGreaterThan(3000)
+
+ await browser.elementByCss('#invalidShallow').click()
+ await waitFor(500)
+ const newScrollPosition3 = await browser.eval('window.pageYOffset')
+
+ expect(newScrollPosition3).toBe(0)
+ })
+ })
+
+ it('should scroll to top when the scroll option is set to true', async () => {
+ const browser = await webdriver(next.appPort, '/nav/shallow-routing')
+ await browser.eval(() =>
+ document.querySelector('#increaseWithScroll').scrollIntoView()
+ )
+ const scrollPosition = await browser.eval('window.pageYOffset')
+
+ expect(scrollPosition).toBeGreaterThan(3000)
+
+ await browser.elementByCss('#increaseWithScroll').click()
+ await check(async () => {
+ const newScrollPosition = await browser.eval('window.pageYOffset')
+ return newScrollPosition === 0 ? 'success' : 'fail'
+ }, 'success')
+ })
+
+ describe('with URL objects', () => {
+ it('should work with ', async () => {
+ const browser = await webdriver(next.appPort, '/nav')
+ const text = await browser
+ .elementByCss('#query-string-link')
+ .click()
+ .waitForElementByCss('.nav-querystring')
+ .elementByCss('p')
+ .text()
+ expect(text).toBe('10')
+
+ expect(await browser.url()).toBe(
+ `http://localhost:${next.appPort}/nav/querystring/10#10`
+ )
+ await browser.close()
+ })
+
+ it('should work with "Router.push"', async () => {
+ const browser = await webdriver(next.appPort, '/nav')
+ const text = await browser
+ .elementByCss('#query-string-button')
+ .click()
+ .waitForElementByCss('.nav-querystring')
+ .elementByCss('p')
+ .text()
+ expect(text).toBe('10')
+
+ expect(await browser.url()).toBe(
+ `http://localhost:${next.appPort}/nav/querystring/10#10`
+ )
+ await browser.close()
+ })
+
+ it('should work with the "replace" prop', async () => {
+ const browser = await webdriver(next.appPort, '/nav')
+
+ let stackLength = await browser.eval('window.history.length')
+
+ expect(stackLength).toBe(2)
+
+ // Navigation to /about using a replace link should maintain the url stack length
+ const text = await browser
+ .elementByCss('#about-replace-link')
+ .click()
+ .waitForElementByCss('.nav-about')
+ .elementByCss('p')
+ .text()
+
+ expect(text).toBe('This is the about page.')
+
+ stackLength = await browser.eval('window.history.length')
+
+ expect(stackLength).toBe(2)
+
+ // Going back to the home with a regular link will augment the history count
+ await browser
+ .elementByCss('#home-link')
+ .click()
+ .waitForElementByCss('.nav-home')
+
+ stackLength = await browser.eval('window.history.length')
+
+ expect(stackLength).toBe(3)
+
+ await browser.close()
+ })
+
+ it('should handle undefined in router.push', async () => {
+ const browser = await webdriver(next.appPort, '/nav/query-params')
+ await browser.elementByCss('#click-me').click()
+ const query = JSON.parse(
+ await browser.waitForElementByCss('#query-value').text()
+ )
+ expect(query).toEqual({
+ param1: '',
+ param2: '',
+ param3: '',
+ param4: '0',
+ param5: 'false',
+ param7: '',
+ param8: '',
+ param9: '',
+ param10: '',
+ param11: ['', '', '', '0', 'false', '', '', '', '', ''],
+ })
+ })
+ })
+
+ describe('with querystring relative urls', () => {
+ it('should work with Link', async () => {
+ const browser = await webdriver(next.appPort, '/nav/query-only')
+ try {
+ await browser.elementByCss('#link').click()
+
+ await check(() => browser.waitForElementByCss('#prop').text(), 'foo')
+ } finally {
+ await browser.close()
+ }
+ })
+
+ it('should work with router.push', async () => {
+ const browser = await webdriver(next.appPort, '/nav/query-only')
+ try {
+ await browser.elementByCss('#router-push').click()
+
+ await check(() => browser.waitForElementByCss('#prop').text(), 'bar')
+ } finally {
+ await browser.close()
+ }
+ })
+
+ it('should work with router.replace', async () => {
+ const browser = await webdriver(next.appPort, '/nav/query-only')
+ try {
+ await browser.elementByCss('#router-replace').click()
+
+ await check(() => browser.waitForElementByCss('#prop').text(), 'baz')
+ } finally {
+ await browser.close()
+ }
+ })
+ })
+
+ describe('with getInitialProp redirect', () => {
+ it('should redirect the page via client side', async () => {
+ const browser = await webdriver(next.appPort, '/nav')
+ const text = await browser
+ .elementByCss('#redirect-link')
+ .click()
+ .waitForElementByCss('.nav-about')
+ .elementByCss('p')
+ .text()
+
+ expect(text).toBe('This is the about page.')
+ await browser.close()
+ })
+
+ it('should redirect the page when loading', async () => {
+ const browser = await webdriver(next.appPort, '/nav/redirect')
+ const text = await browser
+ .waitForElementByCss('.nav-about')
+ .elementByCss('p')
+ .text()
+
+ expect(text).toBe('This is the about page.')
+ await browser.close()
+ })
+ })
+
+ describe('with different types of urls', () => {
+ it('should work with normal page', async () => {
+ const browser = await webdriver(next.appPort, '/with-cdm')
+ const text = await browser.elementByCss('p').text()
+
+ expect(text).toBe('ComponentDidMount executed on client.')
+ await browser.close()
+ })
+
+ it('should work with dir/ page', async () => {
+ const browser = await webdriver(next.appPort, '/nested-cdm')
+ const text = await browser.elementByCss('p').text()
+
+ expect(text).toBe('ComponentDidMount executed on client.')
+ await browser.close()
+ })
+
+ it('should not work with /index page', async () => {
+ const browser = await webdriver(next.appPort, '/index')
+ expect(await browser.elementByCss('h1').text()).toBe('404')
+ expect(await browser.elementByCss('h2').text()).toBe(
+ 'This page could not be found.'
+ )
+ await browser.close()
+ })
+
+ it('should work with / page', async () => {
+ const browser = await webdriver(next.appPort, '/')
+ const text = await browser.elementByCss('p').text()
+
+ expect(text).toBe('ComponentDidMount executed on client.')
+ await browser.close()
+ })
+ })
+
+ describe('with the HOC based router', () => {
+ it('should navigate as expected', async () => {
+ const browser = await webdriver(next.appPort, '/nav/with-hoc')
+
+ const pathname = await browser.elementByCss('#pathname').text()
+ expect(pathname).toBe('Current path: /nav/with-hoc')
+
+ const asPath = await browser.elementByCss('#asPath').text()
+ expect(asPath).toBe('Current asPath: /nav/with-hoc')
+
+ const text = await browser
+ .elementByCss('.nav-with-hoc a')
+ .click()
+ .waitForElementByCss('.nav-home')
+ .elementByCss('p')
+ .text()
+
+ expect(text).toBe('This is the home.')
+ await browser.close()
+ })
+ })
+
+ describe('with asPath', () => {
+ describe('inside getInitialProps', () => {
+ it('should show the correct asPath with a Link with as prop', async () => {
+ const browser = await webdriver(next.appPort, '/nav')
+ const asPath = await browser
+ .elementByCss('#as-path-link')
+ .click()
+ .waitForElementByCss('.as-path-content')
+ .elementByCss('.as-path-content')
+ .text()
+
+ expect(asPath).toBe('/as/path')
+ await browser.close()
+ })
+
+ it('should show the correct asPath with a Link without the as prop', async () => {
+ const browser = await webdriver(next.appPort, '/nav')
+ const asPath = await browser
+ .elementByCss('#as-path-link-no-as')
+ .click()
+ .waitForElementByCss('.as-path-content')
+ .elementByCss('.as-path-content')
+ .text()
+
+ expect(asPath).toBe('/nav/as-path')
+ await browser.close()
+ })
+ })
+
+ describe('with next/router', () => {
+ it('should show the correct asPath', async () => {
+ const browser = await webdriver(next.appPort, '/nav')
+ const asPath = await browser
+ .elementByCss('#as-path-using-router-link')
+ .click()
+ .waitForElementByCss('.as-path-content')
+ .elementByCss('.as-path-content')
+ .text()
+
+ expect(asPath).toBe('/nav/as-path-using-router')
+ await browser.close()
+ })
+
+ it('should navigate an absolute url on push', async () => {
+ const browser = await webdriver(
+ next.appPort,
+ `/absolute-url?port=${next.appPort}`
+ )
+ await browser.waitForElementByCss('#router-push').click()
+ await check(
+ () => browser.eval(() => window.location.origin),
+ 'https://vercel.com'
+ )
+ })
+
+ it('should navigate an absolute url on replace', async () => {
+ const browser = await webdriver(
+ next.appPort,
+ `/absolute-url?port=${next.appPort}`
+ )
+ await browser.waitForElementByCss('#router-replace').click()
+ await check(
+ () => browser.eval(() => window.location.origin),
+ 'https://vercel.com'
+ )
+ })
+
+ it('should navigate an absolute local url on push', async () => {
+ const browser = await webdriver(
+ next.appPort,
+ `/absolute-url?port=${next.appPort}`
+ )
+ // @ts-expect-error _didNotNavigate is set intentionally
+ await browser.eval(() => (window._didNotNavigate = true))
+ await browser.waitForElementByCss('#router-local-push').click()
+ const text = await browser
+ .waitForElementByCss('.nav-about')
+ .elementByCss('p')
+ .text()
+ expect(text).toBe('This is the about page.')
+ // @ts-expect-error _didNotNavigate is set intentionally
+ expect(await browser.eval(() => window._didNotNavigate)).toBe(true)
+ })
+
+ it('should navigate an absolute local url on replace', async () => {
+ const browser = await webdriver(
+ next.appPort,
+ `/absolute-url?port=${next.appPort}`
+ )
+ // @ts-expect-error _didNotNavigate is set intentionally
+ await browser.eval(() => (window._didNotNavigate = true))
+ await browser.waitForElementByCss('#router-local-replace').click()
+ const text = await browser
+ .waitForElementByCss('.nav-about')
+ .elementByCss('p')
+ .text()
+ expect(text).toBe('This is the about page.')
+ // @ts-expect-error _didNotNavigate is set intentionally
+ expect(await browser.eval(() => window._didNotNavigate)).toBe(true)
+ })
+ })
+
+ describe('with next/link', () => {
+ it('should use pushState with same href and different asPath', async () => {
+ let browser
+ try {
+ browser = await webdriver(next.appPort, '/nav/as-path-pushstate')
+ await browser
+ .elementByCss('#hello')
+ .click()
+ .waitForElementByCss('#something-hello')
+ const queryOne = JSON.parse(
+ await browser.elementByCss('#router-query').text()
+ )
+ expect(queryOne.something).toBe('hello')
+ await browser
+ .elementByCss('#same-query')
+ .click()
+ .waitForElementByCss('#something-same-query')
+ const queryTwo = JSON.parse(
+ await browser.elementByCss('#router-query').text()
+ )
+ expect(queryTwo.something).toBe('hello')
+ await browser.back().waitForElementByCss('#something-hello')
+ const queryThree = JSON.parse(
+ await browser.elementByCss('#router-query').text()
+ )
+ expect(queryThree.something).toBe('hello')
+ await browser
+ .elementByCss('#else')
+ .click()
+ .waitForElementByCss('#something-else')
+ await browser
+ .elementByCss('#hello2')
+ .click()
+ .waitForElementByCss('#nav-as-path-pushstate')
+ await browser.back().waitForElementByCss('#something-else')
+ const queryFour = JSON.parse(
+ await browser.elementByCss('#router-query').text()
+ )
+ expect(queryFour.something).toBe(undefined)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should detect asPath query changes correctly', async () => {
+ let browser
+ try {
+ browser = await webdriver(next.appPort, '/nav/as-path-query')
+ await browser
+ .elementByCss('#hello')
+ .click()
+ .waitForElementByCss('#something-hello-something-hello')
+ const queryOne = JSON.parse(
+ await browser.elementByCss('#router-query').text()
+ )
+ expect(queryOne.something).toBe('hello')
+ await browser
+ .elementByCss('#hello2')
+ .click()
+ .waitForElementByCss('#something-hello-something-else')
+ const queryTwo = JSON.parse(
+ await browser.elementByCss('#router-query').text()
+ )
+ expect(queryTwo.something).toBe('else')
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+ })
+ })
+
+ describe('runtime errors', () => {
+ it('should show redbox when a client side error is thrown inside a component', async () => {
+ let browser
+ try {
+ browser = await webdriver(next.appPort, '/error-inside-browser-page')
+ expect(await hasRedbox(browser, true)).toBe(true)
+ const text = await getRedboxSource(browser)
+ expect(text).toMatch(/An Expected error occurred/)
+ expect(text).toMatch(
+ /pages[\\/]error-inside-browser-page\.js \(5:12\)/
+ )
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should show redbox when a client side error is thrown outside a component', async () => {
+ let browser
+ try {
+ browser = await webdriver(
+ next.appPort,
+ '/error-in-the-browser-global-scope'
+ )
+ expect(await hasRedbox(browser, true)).toBe(true)
+ const text = await getRedboxSource(browser)
+ expect(text).toMatch(/An Expected error occurred/)
+ expect(text).toMatch(/error-in-the-browser-global-scope\.js \(2:8\)/)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+ })
+
+ describe('with 404 pages', () => {
+ it('should 404 on not existent page', async () => {
+ const browser = await webdriver(next.appPort, '/non-existent')
+ expect(await browser.elementByCss('h1').text()).toBe('404')
+ expect(await browser.elementByCss('h2').text()).toBe(
+ 'This page could not be found.'
+ )
+ await browser.close()
+ })
+
+ it('should 404 on wrong casing', async () => {
+ const browser = await webdriver(next.appPort, '/nAv/AbOuT')
+ expect(await browser.elementByCss('h1').text()).toBe('404')
+ expect(await browser.elementByCss('h2').text()).toBe(
+ 'This page could not be found.'
+ )
+ await browser.close()
+ })
+
+ it('should get url dynamic param', async () => {
+ const browser = await webdriver(
+ next.appPort,
+ '/dynamic/dynamic-part/route'
+ )
+ expect(await browser.elementByCss('p').text()).toBe('dynamic-part')
+ await browser.close()
+ })
+
+ it('should 404 on wrong casing of url dynamic param', async () => {
+ const browser = await webdriver(
+ next.appPort,
+ '/dynamic/dynamic-part/RoUtE'
+ )
+ expect(await browser.elementByCss('h1').text()).toBe('404')
+ expect(await browser.elementByCss('h2').text()).toBe(
+ 'This page could not be found.'
+ )
+ await browser.close()
+ })
+
+ it('should not 404 for /', async () => {
+ const browser = await webdriver(next.appPort, '/nav/about/')
+ const text = await browser.elementByCss('p').text()
+ expect(text).toBe('This is the about page.')
+ await browser.close()
+ })
+
+ it('should should not contain a page script in a 404 page', async () => {
+ const browser = await webdriver(next.appPort, '/non-existent')
+ const scripts = await browser.elementsByCss('script[src]')
+ for (const script of scripts) {
+ const src = await script.getAttribute('src')
+ expect(src.includes('/non-existent')).toBeFalsy()
+ }
+ await browser.close()
+ })
+ })
+
+ describe('updating head while client routing', () => {
+ it('should only execute async and defer scripts once', async () => {
+ let browser
+ try {
+ browser = await webdriver(next.appPort, '/head')
+
+ await browser.waitForElementByCss('h1')
+ await waitFor(2000)
+ expect(
+ Number(await browser.eval('window.__test_async_executions'))
+ ).toBe(1)
+ expect(
+ Number(await browser.eval('window.__test_defer_executions'))
+ ).toBe(1)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+
+ it('should warn when stylesheets or scripts are in head', async () => {
+ let browser
+ try {
+ browser = await webdriver(next.appPort, '/head')
+
+ await browser.waitForElementByCss('h1')
+ await waitFor(1000)
+ const browserLogs = await browser.log('browser')
+ let foundStyles = false
+ let foundScripts = false
+ const logs = []
+ browserLogs.forEach(({ message }) => {
+ if (message.includes('Do not add stylesheets using next/head')) {
+ foundStyles = true
+ logs.push(message)
+ }
+ if (message.includes('Do not add