diff --git a/e2e/react-router/basepath-file-based/src/routeTree.gen.ts b/e2e/react-router/basepath-file-based/src/routeTree.gen.ts index 177ee7a3136..2989a394d3a 100644 --- a/e2e/react-router/basepath-file-based/src/routeTree.gen.ts +++ b/e2e/react-router/basepath-file-based/src/routeTree.gen.ts @@ -9,11 +9,17 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' +import { Route as ScrollErrorRouteImport } from './routes/scroll-error' import { Route as RedirectReloadRouteImport } from './routes/redirectReload' import { Route as RedirectRouteImport } from './routes/redirect' import { Route as AboutRouteImport } from './routes/about' import { Route as IndexRouteImport } from './routes/index' +const ScrollErrorRoute = ScrollErrorRouteImport.update({ + id: '/scroll-error', + path: '/scroll-error', + getParentRoute: () => rootRouteImport, +} as any) const RedirectReloadRoute = RedirectReloadRouteImport.update({ id: '/redirectReload', path: '/redirectReload', @@ -40,12 +46,14 @@ export interface FileRoutesByFullPath { '/about': typeof AboutRoute '/redirect': typeof RedirectRoute '/redirectReload': typeof RedirectReloadRoute + '/scroll-error': typeof ScrollErrorRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/about': typeof AboutRoute '/redirect': typeof RedirectRoute '/redirectReload': typeof RedirectReloadRoute + '/scroll-error': typeof ScrollErrorRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -53,13 +61,20 @@ export interface FileRoutesById { '/about': typeof AboutRoute '/redirect': typeof RedirectRoute '/redirectReload': typeof RedirectReloadRoute + '/scroll-error': typeof ScrollErrorRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/about' | '/redirect' | '/redirectReload' + fullPaths: '/' | '/about' | '/redirect' | '/redirectReload' | '/scroll-error' fileRoutesByTo: FileRoutesByTo - to: '/' | '/about' | '/redirect' | '/redirectReload' - id: '__root__' | '/' | '/about' | '/redirect' | '/redirectReload' + to: '/' | '/about' | '/redirect' | '/redirectReload' | '/scroll-error' + id: + | '__root__' + | '/' + | '/about' + | '/redirect' + | '/redirectReload' + | '/scroll-error' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -67,10 +82,18 @@ export interface RootRouteChildren { AboutRoute: typeof AboutRoute RedirectRoute: typeof RedirectRoute RedirectReloadRoute: typeof RedirectReloadRoute + ScrollErrorRoute: typeof ScrollErrorRoute } declare module '@tanstack/react-router' { interface FileRoutesByPath { + '/scroll-error': { + id: '/scroll-error' + path: '/scroll-error' + fullPath: '/scroll-error' + preLoaderRoute: typeof ScrollErrorRouteImport + parentRoute: typeof rootRouteImport + } '/redirectReload': { id: '/redirectReload' path: '/redirectReload' @@ -107,6 +130,7 @@ const rootRouteChildren: RootRouteChildren = { AboutRoute: AboutRoute, RedirectRoute: RedirectRoute, RedirectReloadRoute: RedirectReloadRoute, + ScrollErrorRoute: ScrollErrorRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/e2e/react-router/basepath-file-based/src/routes/scroll-error.tsx b/e2e/react-router/basepath-file-based/src/routes/scroll-error.tsx new file mode 100644 index 00000000000..227c152172e --- /dev/null +++ b/e2e/react-router/basepath-file-based/src/routes/scroll-error.tsx @@ -0,0 +1,16 @@ +import { Link, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/scroll-error')({ + component: ScrollErrorComponent, +}) + +function ScrollErrorComponent() { + return ( +
+ About +
+

Scroll Error Test

+
+
+ ) +} diff --git a/e2e/react-router/basepath-file-based/tests/scroll-restoration-session-storage-error.test.ts b/e2e/react-router/basepath-file-based/tests/scroll-restoration-session-storage-error.test.ts new file mode 100644 index 00000000000..ab0bb4bc993 --- /dev/null +++ b/e2e/react-router/basepath-file-based/tests/scroll-restoration-session-storage-error.test.ts @@ -0,0 +1,101 @@ +/* eslint-disable */ +import { expect, test } from '@playwright/test' +import type { Page } from '@playwright/test' + +const trackConsole = (page: Page) => { + const consoleWarnings: Array = [] + + page.on('console', (msg) => { + if (msg.type() === 'warning') { + consoleWarnings.push(msg.text()) + } + }) + + return consoleWarnings +} + +test.describe('Scroll Restoration with Session Storage Error', () => { + test('should not crash when sessionStorage.setItem throws an error', async ({ + page, + }) => { + const consoleWarnings = trackConsole(page) + + await page.goto('/app/scroll-error') + await page.waitForLoadState('networkidle') + + await page.evaluate(() => { + sessionStorage.setItem = () => { + throw new Error('Test Error') + } + }) + + await page.evaluate(() => window.scrollTo(0, 200)) + await page.waitForTimeout(150) + + await page.click('a[href="/app/about"]') + await page.waitForLoadState('networkidle') + + await page.goBack() + await page.waitForLoadState('networkidle') + + expect( + consoleWarnings.some((warning) => + warning.includes( + '[ts-router] Could not persist scroll restoration state to sessionStorage.', + ), + ), + ).toBeTruthy() + + const heading = page.locator('h1:has-text("Scroll Error Test")') + await expect(heading).toBeVisible() + + const scrollPosition = await page.evaluate(() => window.scrollY) + expect(scrollPosition).not.toBe(200) + }) + + test('should surface warning when sessionStorage quota is exceeded', async ({ + page, + }) => { + const consoleWarnings = trackConsole(page) + + await page.goto('/app/scroll-error') + await page.waitForLoadState('networkidle') + + await page.evaluate(() => { + let i = 0 + const chunk = 'x'.repeat(32) + + try { + while (true) { + sessionStorage.setItem(`key_${i}`, chunk) + i += 1 + } + } catch { + console.log(`Stored ${i} keys in session storage`) + } + }) + + await page.evaluate(() => window.scrollTo(0, 200)) + await page.waitForTimeout(150) + + await page.click('a[href="/app/about"]') + await page.waitForLoadState('networkidle') + + await page.goBack() + await page.waitForLoadState('networkidle') + + expect( + consoleWarnings.some((warning) => + warning.includes( + '[ts-router] Could not persist scroll restoration state to sessionStorage.', + ), + ), + ).toBeTruthy() + + const heading = page.locator('h1:has-text("Scroll Error Test")') + await expect(heading).toBeVisible() + + const scrollPosition = await page.evaluate(() => window.scrollY) + expect(scrollPosition).not.toBe(200) + }) +}) diff --git a/packages/router-core/src/scroll-restoration.ts b/packages/router-core/src/scroll-restoration.ts index 22d5ecb558d..c8614429a9b 100644 --- a/packages/router-core/src/scroll-restoration.ts +++ b/packages/router-core/src/scroll-restoration.ts @@ -66,10 +66,16 @@ function createScrollRestorationCache(): ScrollRestorationCache | null { // This setter is simply to make sure that we set the sessionStorage right // after the state is updated. It doesn't necessarily need to be a functional // update. - set: (updater) => ( - (state = functionalUpdate(updater, state) || state), - safeSessionStorage.setItem(storageKey, JSON.stringify(state)) - ), + set: (updater) => { + state = functionalUpdate(updater, state) || state + try { + safeSessionStorage.setItem(storageKey, JSON.stringify(state)) + } catch { + console.warn( + '[ts-router] Could not persist scroll restoration state to sessionStorage.', + ) + } + }, } }