From 5739edc7cf5e9a3ba41054c5c701e8ddbe7607ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Born=C3=B6?= Date: Sun, 29 May 2022 03:07:44 +0200 Subject: [PATCH] Ignore popstate with invalid state (#37110) * Ignore popstate with invalid state * Make tests pass * i18n case fixed * Fix lint error * Unhandled Promise Rejection * Revert "Unhandled Promise Rejection" This reverts commit ac4fde7654ed549822185ab0229a6d01c6ea194f. * fix type check Co-authored-by: JJ Kasper --- packages/next/shared/lib/router/router.ts | 15 ++- .../app/next.config.js | 6 ++ .../app/pages/[dynamic].js | 3 + .../app/pages/static.js | 3 + .../with-i18n.test.ts | 94 +++++++++++++++++++ .../without-i18n.test.ts | 77 +++++++++++++++ tsconfig.json | 7 +- 7 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 test/e2e/ignore-invalid-popstateevent/app/next.config.js create mode 100644 test/e2e/ignore-invalid-popstateevent/app/pages/[dynamic].js create mode 100644 test/e2e/ignore-invalid-popstateevent/app/pages/static.js create mode 100644 test/e2e/ignore-invalid-popstateevent/with-i18n.test.ts create mode 100644 test/e2e/ignore-invalid-popstateevent/without-i18n.test.ts diff --git a/packages/next/shared/lib/router/router.ts b/packages/next/shared/lib/router/router.ts index 89be752042a39..a23c03d03b193 100644 --- a/packages/next/shared/lib/router/router.ts +++ b/packages/next/shared/lib/router/router.ts @@ -92,7 +92,7 @@ type PreflightEffect = | { type: 'refresh' } | { type: 'next' } -type HistoryState = +export type HistoryState = | null | { __N: false } | ({ __N: true; key: string } & NextHistoryState) @@ -642,6 +642,7 @@ export default class Router implements BaseRouter { domainLocales?: DomainLocale[] | undefined isReady: boolean isLocaleDomain: boolean + isFirstPopStateEvent = true private state: Readonly<{ route: string @@ -797,6 +798,9 @@ export default class Router implements BaseRouter { } onPopState = (e: PopStateEvent): void => { + const { isFirstPopStateEvent } = this + this.isFirstPopStateEvent = false + const state = e.state as HistoryState if (!state) { @@ -822,6 +826,15 @@ export default class Router implements BaseRouter { return } + // Safari fires popstateevent when reopening the browser. + if ( + isFirstPopStateEvent && + this.locale === state.options.locale && + state.as === this.asPath + ) { + return + } + let forcedScroll: { x: number; y: number } | undefined const { url, as, options, key } = state if (process.env.__NEXT_SCROLL_RESTORATION) { diff --git a/test/e2e/ignore-invalid-popstateevent/app/next.config.js b/test/e2e/ignore-invalid-popstateevent/app/next.config.js new file mode 100644 index 0000000000000..1478f3c05a256 --- /dev/null +++ b/test/e2e/ignore-invalid-popstateevent/app/next.config.js @@ -0,0 +1,6 @@ +module.exports = { + i18n: { + locales: ['en', 'sv'], + defaultLocale: 'en', + }, +} diff --git a/test/e2e/ignore-invalid-popstateevent/app/pages/[dynamic].js b/test/e2e/ignore-invalid-popstateevent/app/pages/[dynamic].js new file mode 100644 index 0000000000000..b3ecc6dcc3065 --- /dev/null +++ b/test/e2e/ignore-invalid-popstateevent/app/pages/[dynamic].js @@ -0,0 +1,3 @@ +export default function DynamicPage() { + return

dynamic

+} diff --git a/test/e2e/ignore-invalid-popstateevent/app/pages/static.js b/test/e2e/ignore-invalid-popstateevent/app/pages/static.js new file mode 100644 index 0000000000000..c5a85c3c1c9c3 --- /dev/null +++ b/test/e2e/ignore-invalid-popstateevent/app/pages/static.js @@ -0,0 +1,3 @@ +export default function StaticPage() { + return

static

+} diff --git a/test/e2e/ignore-invalid-popstateevent/with-i18n.test.ts b/test/e2e/ignore-invalid-popstateevent/with-i18n.test.ts new file mode 100644 index 0000000000000..370804df93401 --- /dev/null +++ b/test/e2e/ignore-invalid-popstateevent/with-i18n.test.ts @@ -0,0 +1,94 @@ +import { join } from 'path' +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { check, waitFor } from 'next-test-utils' +import webdriver from 'next-webdriver' + +import type { HistoryState } from '../../../packages/next/shared/lib/router/router' +import { BrowserInterface } from 'test/lib/browsers/base' + +const emitPopsStateEvent = (browser: BrowserInterface, state: HistoryState) => + browser.eval( + `window.dispatchEvent(new PopStateEvent("popstate", { state: ${JSON.stringify( + state + )} }))` + ) + +describe('i18n: Event with stale state - static route previously was dynamic', () => { + 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()) + + test('Ignore event without query param', async () => { + const browser = await webdriver(next.url, '/sv/static') + browser.close() + + const state: HistoryState = { + url: '/[dynamic]?', + as: '/static', + options: { locale: 'sv' }, + __N: true, + key: '', + } + + expect(await browser.elementByCss('#page-type').text()).toBe('static') + + // 1st event is ignored + await emitPopsStateEvent(browser, state) + await waitFor(1000) + expect(await browser.elementByCss('#page-type').text()).toBe('static') + + // 2nd event isn't ignored + await emitPopsStateEvent(browser, state) + await check(() => browser.elementByCss('#page-type').text(), 'dynamic') + }) + + test('Ignore event with query param', async () => { + const browser = await webdriver(next.url, '/sv/static?param=1') + + const state: HistoryState = { + url: '/[dynamic]?param=1', + as: '/static?param=1', + options: { locale: 'sv' }, + __N: true, + key: '', + } + + expect(await browser.elementByCss('#page-type').text()).toBe('static') + + // 1st event is ignored + await emitPopsStateEvent(browser, state) + await waitFor(1000) + expect(await browser.elementByCss('#page-type').text()).toBe('static') + + // 2nd event isn't ignored + await emitPopsStateEvent(browser, state) + await check(() => browser.elementByCss('#page-type').text(), 'dynamic') + }) + + test("Don't ignore event with different locale", async () => { + const browser = await webdriver(next.url, '/sv/static?param=1') + + const state: HistoryState = { + url: '/[dynamic]?param=1', + as: '/static?param=1', + options: { locale: 'en' }, + __N: true, + key: '', + } + + expect(await browser.elementByCss('#page-type').text()).toBe('static') + + await emitPopsStateEvent(browser, state) + await check(() => browser.elementByCss('#page-type').text(), 'dynamic') + }) +}) diff --git a/test/e2e/ignore-invalid-popstateevent/without-i18n.test.ts b/test/e2e/ignore-invalid-popstateevent/without-i18n.test.ts new file mode 100644 index 0000000000000..b3c3a0393ea7d --- /dev/null +++ b/test/e2e/ignore-invalid-popstateevent/without-i18n.test.ts @@ -0,0 +1,77 @@ +import { join } from 'path' +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { check, waitFor } from 'next-test-utils' +import webdriver from 'next-webdriver' + +import type { HistoryState } from '../../../packages/next/shared/lib/router/router' +import { BrowserInterface } from 'test/lib/browsers/base' + +const emitPopsStateEvent = (browser: BrowserInterface, state: HistoryState) => + browser.eval( + `window.dispatchEvent(new PopStateEvent("popstate", { state: ${JSON.stringify( + state + )} }))` + ) + +describe('Event with stale state - static route previously was dynamic', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: { + pages: new FileRef(join(__dirname, 'app/pages')), + // Don't use next.config.js to avoid getting i18n + }, + dependencies: {}, + }) + }) + afterAll(() => next.destroy()) + + test('Ignore event without query param', async () => { + const browser = await webdriver(next.url, '/static') + browser.close() + + const state: HistoryState = { + url: '/[dynamic]?', + as: '/static', + options: {}, + __N: true, + key: '', + } + + expect(await browser.elementByCss('#page-type').text()).toBe('static') + + // 1st event is ignored + await emitPopsStateEvent(browser, state) + await waitFor(1000) + expect(await browser.elementByCss('#page-type').text()).toBe('static') + + // 2nd event isn't ignored + await emitPopsStateEvent(browser, state) + await check(() => browser.elementByCss('#page-type').text(), 'dynamic') + }) + + test('Ignore event with query param', async () => { + const browser = await webdriver(next.url, '/static?param=1') + + const state: HistoryState = { + url: '/[dynamic]?param=1', + as: '/static?param=1', + options: {}, + __N: true, + key: '', + } + + expect(await browser.elementByCss('#page-type').text()).toBe('static') + + // 1st event is ignored + await emitPopsStateEvent(browser, state) + await waitFor(1000) + expect(await browser.elementByCss('#page-type').text()).toBe('static') + + // 2nd event isn't ignored + await emitPopsStateEvent(browser, state) + await check(() => browser.elementByCss('#page-type').text(), 'dynamic') + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 19c2ce7e417d7..8609c1ea78fd5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,5 +17,10 @@ "e2e-utils": ["test/lib/e2e-utils"] } }, - "include": ["test/**/*.test.ts", "test/**/*.test.tsx", "test/**/test/*"] + "include": [ + "test/**/*.test.ts", + "test/**/*.test.tsx", + "test/**/test/*", + "packages/next/types/webpack.d.ts" + ] }