Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
8 changes: 8 additions & 0 deletions .changeset/tall-buses-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@tanstack/router-core': patch
'@tanstack/react-router': patch
'@tanstack/solid-router': patch
'@tanstack/vue-router': patch
---

fix: scroll restoration without throttling
Original file line number Diff line number Diff line change
@@ -1,25 +1,10 @@
/* 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')

Expand All @@ -32,32 +17,19 @@ test.describe('Scroll Restoration with Session Storage 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.reload()
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 ({
test('should not crash when sessionStorage quota is exceeded', async ({
page,
}) => {
const consoleWarnings = trackConsole(page)

await page.goto('/app/scroll-error')
await page.waitForLoadState('networkidle')

Expand All @@ -78,20 +50,9 @@ test.describe('Scroll Restoration with Session Storage Error', () => {
await page.evaluate(() => window.scrollTo(0, 200))
await page.waitForTimeout(150)

await page.click('a[href="/app/about"]')
await page.reload()
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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { Route as testsPageWithSearchRouteImport } from './routes/(tests)/page-w
import { Route as testsNormalPageRouteImport } from './routes/(tests)/normal-page'
import { Route as testsLazyWithLoaderPageRouteImport } from './routes/(tests)/lazy-with-loader-page'
import { Route as testsLazyPageRouteImport } from './routes/(tests)/lazy-page'
import { Route as testsIssue7040TargetRouteImport } from './routes/(tests)/issue-7040-target'
import { Route as testsIssue7040SourceRouteImport } from './routes/(tests)/issue-7040-source'

const testsVirtualPageLazyRouteImport = createFileRoute(
'/(tests)/virtual-page',
Expand Down Expand Up @@ -59,9 +61,21 @@ const testsLazyPageRoute = testsLazyPageRouteImport
getParentRoute: () => rootRouteImport,
} as any)
.lazy(() => import('./routes/(tests)/lazy-page.lazy').then((d) => d.Route))
const testsIssue7040TargetRoute = testsIssue7040TargetRouteImport.update({
id: '/(tests)/issue-7040-target',
path: '/issue-7040-target',
getParentRoute: () => rootRouteImport,
} as any)
const testsIssue7040SourceRoute = testsIssue7040SourceRouteImport.update({
id: '/(tests)/issue-7040-source',
path: '/issue-7040-source',
getParentRoute: () => rootRouteImport,
} as any)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/issue-7040-source': typeof testsIssue7040SourceRoute
'/issue-7040-target': typeof testsIssue7040TargetRoute
'/lazy-page': typeof testsLazyPageRoute
'/lazy-with-loader-page': typeof testsLazyWithLoaderPageRoute
'/normal-page': typeof testsNormalPageRoute
Expand All @@ -70,6 +84,8 @@ export interface FileRoutesByFullPath {
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/issue-7040-source': typeof testsIssue7040SourceRoute
'/issue-7040-target': typeof testsIssue7040TargetRoute
'/lazy-page': typeof testsLazyPageRoute
'/lazy-with-loader-page': typeof testsLazyWithLoaderPageRoute
'/normal-page': typeof testsNormalPageRoute
Expand All @@ -79,6 +95,8 @@ export interface FileRoutesByTo {
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/(tests)/issue-7040-source': typeof testsIssue7040SourceRoute
'/(tests)/issue-7040-target': typeof testsIssue7040TargetRoute
'/(tests)/lazy-page': typeof testsLazyPageRoute
'/(tests)/lazy-with-loader-page': typeof testsLazyWithLoaderPageRoute
'/(tests)/normal-page': typeof testsNormalPageRoute
Expand All @@ -89,6 +107,8 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/issue-7040-source'
| '/issue-7040-target'
| '/lazy-page'
| '/lazy-with-loader-page'
| '/normal-page'
Expand All @@ -97,6 +117,8 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/issue-7040-source'
| '/issue-7040-target'
| '/lazy-page'
| '/lazy-with-loader-page'
| '/normal-page'
Expand All @@ -105,6 +127,8 @@ export interface FileRouteTypes {
id:
| '__root__'
| '/'
| '/(tests)/issue-7040-source'
| '/(tests)/issue-7040-target'
| '/(tests)/lazy-page'
| '/(tests)/lazy-with-loader-page'
| '/(tests)/normal-page'
Expand All @@ -114,6 +138,8 @@ export interface FileRouteTypes {
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
testsIssue7040SourceRoute: typeof testsIssue7040SourceRoute
testsIssue7040TargetRoute: typeof testsIssue7040TargetRoute
testsLazyPageRoute: typeof testsLazyPageRoute
testsLazyWithLoaderPageRoute: typeof testsLazyWithLoaderPageRoute
testsNormalPageRoute: typeof testsNormalPageRoute
Expand Down Expand Up @@ -165,11 +191,27 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof testsLazyPageRouteImport
parentRoute: typeof rootRouteImport
}
'/(tests)/issue-7040-target': {
id: '/(tests)/issue-7040-target'
path: '/issue-7040-target'
fullPath: '/issue-7040-target'
preLoaderRoute: typeof testsIssue7040TargetRouteImport
parentRoute: typeof rootRouteImport
}
'/(tests)/issue-7040-source': {
id: '/(tests)/issue-7040-source'
path: '/issue-7040-source'
fullPath: '/issue-7040-source'
preLoaderRoute: typeof testsIssue7040SourceRouteImport
parentRoute: typeof rootRouteImport
}
}
}

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
testsIssue7040SourceRoute: testsIssue7040SourceRoute,
testsIssue7040TargetRoute: testsIssue7040TargetRoute,
testsLazyPageRoute: testsLazyPageRoute,
testsLazyWithLoaderPageRoute: testsLazyWithLoaderPageRoute,
testsNormalPageRoute: testsNormalPageRoute,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as React from 'react'
import { Link, createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/(tests)/issue-7040-source')({
component: Component,
})

const sectionCount = 180

function Component() {
return (
<div className="p-2">
<div className="grid gap-2">
<h3>issue-7040-source</h3>
<p data-testid="issue-7040-source-top">
Tall page for reproducing the scroll restoration race.
</p>
</div>

<Link
to="/issue-7040-target"
resetScroll
data-testid="issue-7040-target-link"
className="fixed right-4 bottom-4 z-50 rounded bg-blue-600 px-4 py-2 font-medium text-white shadow-lg"
>
Go to issue-7040-target
</Link>

<div className="mt-4 grid gap-3">
{Array.from({ length: sectionCount }).map((_, index) => (
<section
key={`issue-7040-source-section-${index}`}
className="min-h-24 rounded border p-3"
>
<h4 className="font-medium">Section {index + 1}</h4>
<p>
This section adds vertical space so the page can be scrolled far
enough to reproduce issue #7040.
</p>
</section>
))}
</div>

<div data-testid="issue-7040-source-bottom" className="py-8">
Bottom of issue-7040-source
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as React from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { sleep } from '../../posts'

export const Route = createFileRoute('/(tests)/issue-7040-target')({
loader: async () => {
await sleep(250)
return null
},
component: Component,
})

function Component() {
return (
<div className="p-2">
<div className="grid gap-2">
<h3>issue-7040-target</h3>
<p data-testid="issue-7040-target-top">
Short page that should reset to top on navigation.
</p>
</div>

<div className="mt-4 grid gap-3">
{Array.from({ length: 18 }).map((_, index) => (
<section
key={`issue-7040-target-section-${index}`}
className="min-h-16 rounded border p-3"
>
<h4 className="font-medium">Target section {index + 1}</h4>
<p>This page is intentionally much shorter than the source page.</p>
</section>
))}
</div>

<div data-testid="issue-7040-target-bottom" className="py-8">
Bottom of issue-7040-target
</div>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ function Nav({ type }: { type: 'header' | 'footer' }) {
linkOptions({ to: '/virtual-page' }),
linkOptions({ to: '/lazy-with-loader-page' }),
linkOptions({ to: '/page-with-search', search: { where: type } }),
linkOptions({ to: '/issue-7040-source' }),
linkOptions({ to: '/issue-7040-target' }),
] as const
).map((options, i) => (
<Link
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ function HomeComponent() {
</p>
{(
[
linkOptions({ to: '/issue-7040-source' }),
linkOptions({ to: '/issue-7040-target' }),
linkOptions({ to: '/normal-page' }),
linkOptions({ to: '/lazy-page' }),
linkOptions({ to: '/virtual-page' }),
Expand Down
Loading
Loading