Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions e2e/react-router/basepath-file-based/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -40,37 +46,54 @@ 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
'/': typeof IndexRoute
'/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 {
IndexRoute: typeof IndexRoute
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'
Expand Down Expand Up @@ -107,6 +130,7 @@ const rootRouteChildren: RootRouteChildren = {
AboutRoute: AboutRoute,
RedirectRoute: RedirectRoute,
RedirectReloadRoute: RedirectReloadRoute,
ScrollErrorRoute: ScrollErrorRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
Expand Down
16 changes: 16 additions & 0 deletions e2e/react-router/basepath-file-based/src/routes/scroll-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Link, createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/scroll-error')({
component: ScrollErrorComponent,
})

function ScrollErrorComponent() {
return (
<div>
<Link to="/about">About</Link>
<div style={{ height: '2000px' }}>
<h1>Scroll Error Test</h1>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -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<string> = []

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)
})
})
14 changes: 10 additions & 4 deletions packages/router-core/src/scroll-restoration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
)
}
},
}
}

Expand Down
Loading