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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Link, createFileRoute } from '@tanstack/react-router'
import { Suspense, useMemo } from 'react'
import { Suspense } from 'react'
import { queryOptions, useQuery } from '@tanstack/react-query'
import { z } from 'zod'

Expand All @@ -12,7 +12,7 @@ const doubleQueryOptions = (n: number) =>
queryKey: ['transition-double', n],
queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return n * 2
return { n, double: n * 2 }
},
placeholderData: (oldData) => oldData,
})
Expand Down Expand Up @@ -44,8 +44,10 @@ function TransitionPage() {
</Link>

<div className="mt-2">
<div data-testid="n-value">n: {search.n}</div>
<div data-testid="double-value">double: {doubleQuery.data}</div>
<div data-testid="n-value">n: {doubleQuery.data?.n}</div>
<div data-testid="double-value">
double: {doubleQuery.data?.double}
</div>
</div>
</div>
</Suspense>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,95 @@
import { expect, test } from '@playwright/test'

test('react-query transitions keep previous data during navigation', async ({
test('transitions/count/query should keep old values visible during navigation', async ({
page,
}) => {
await page.goto('/transition/count/query')

await expect(page.getByTestId('n-value')).toContainText('n: 1')
await expect(page.getByTestId('double-value')).toContainText('double: 2')

const bodySnapshots: Array<string> = []
const bodyTexts: Array<string> = []

const interval = setInterval(async () => {
const pollInterval = setInterval(async () => {
const text = await page
.locator('body')
.textContent()
.catch(() => '')
if (text) bodySnapshots.push(text)
if (text) bodyTexts.push(text)
}, 50)

await page.getByTestId('increase-button').click()
// 1 click

page.getByTestId('increase-button').click()

await expect(page.getByTestId('n-value')).toContainText('n: 1', {
timeout: 2_000,
})
await expect(page.getByTestId('double-value')).toContainText('double: 2', {
timeout: 2_000,
})

await page.waitForTimeout(200)

clearInterval(interval)
await expect(page.getByTestId('n-value')).toContainText('n: 2', {
timeout: 2000,
})
await expect(page.getByTestId('double-value')).toContainText('double: 4', {
timeout: 2000,
})

// 2 clicks

page.getByTestId('increase-button').click()
page.getByTestId('increase-button').click()

await expect(page.getByTestId('n-value')).toContainText('n: 2', {
timeout: 2_000,
timeout: 2000,
})
await expect(page.getByTestId('double-value')).toContainText('double: 4', {
timeout: 2_000,
timeout: 2000,
})

await page.waitForTimeout(200)

await expect(page.getByTestId('n-value')).toContainText('n: 4', {
timeout: 2000,
})
await expect(page.getByTestId('double-value')).toContainText('double: 8', {
timeout: 2000,
})

// 3 clicks

page.getByTestId('increase-button').click()
page.getByTestId('increase-button').click()
page.getByTestId('increase-button').click()

await expect(page.getByTestId('n-value')).toContainText('n: 4', {
timeout: 2000,
})
await expect(page.getByTestId('double-value')).toContainText('double: 8', {
timeout: 2000,
})

await page.waitForTimeout(200)

await expect(page.getByTestId('n-value')).toContainText('n: 7', {
timeout: 2000,
})
await expect(page.getByTestId('double-value')).toContainText('double: 14', {
timeout: 2000,
})

clearInterval(pollInterval)

const sawLoading = bodySnapshots.some((text) => text.includes('Loading...'))
// With proper transitions, old values should remain visible until new ones arrive
const hasLoadingText = bodyTexts.some((text) => text.includes('Loading...'))

expect(sawLoading).toBeFalsy()
if (hasLoadingText) {
throw new Error(
'FAILED: "Loading..." appeared during navigation. ' +
'Solid Router should use transitions to keep old values visible.',
)
}
Comment on lines +89 to +94
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Error message references wrong framework.

The error message mentions "Solid Router" but this is a React Router test. This appears to be a copy-paste error from the solid-router equivalent test.

Apply this diff:

 if (hasLoadingText) {
   throw new Error(
     'FAILED: "Loading..." appeared during navigation. ' +
-      'Solid Router should use transitions to keep old values visible.',
+      'React Router should use transitions to keep old values visible.',
   )
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (hasLoadingText) {
throw new Error(
'FAILED: "Loading..." appeared during navigation. ' +
'Solid Router should use transitions to keep old values visible.',
)
}
if (hasLoadingText) {
throw new Error(
'FAILED: "Loading..." appeared during navigation. ' +
'React Router should use transitions to keep old values visible.',
)
}
🤖 Prompt for AI Agents
In e2e/react-router/basic-react-query-file-based/tests/transition.spec.ts around
lines 89 to 94, the thrown error message incorrectly references "Solid Router";
update the string to reference "React Router" instead. Replace the message so it
reads something like: 'FAILED: "Loading..." appeared during navigation. React
Router should use transitions to keep old values visible.' and keep the rest of
the throw logic unchanged.

})
3 changes: 2 additions & 1 deletion e2e/react-start/basic-react-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"react-dom": "^19.0.0",
"redaxios": "^0.5.1",
"tailwind-merge": "^2.6.0",
"vite": "^7.1.7"
"vite": "^7.1.7",
"zod": "^4.1.12"
},
"devDependencies": {
"@playwright/test": "^1.50.1",
Expand Down
21 changes: 21 additions & 0 deletions e2e/react-start/basic-react-query/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
import { Route as ApiUsersRouteImport } from './routes/api.users'
import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
import { Route as TransitionCountQueryRouteImport } from './routes/transition/count/query'
import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
import { Route as ApiUsersIdRouteImport } from './routes/api/users.$id'
import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
Expand Down Expand Up @@ -90,6 +91,11 @@ const LayoutLayout2Route = LayoutLayout2RouteImport.update({
id: '/_layout-2',
getParentRoute: () => LayoutRoute,
} as any)
const TransitionCountQueryRoute = TransitionCountQueryRouteImport.update({
id: '/transition/count/query',
path: '/transition/count/query',
getParentRoute: () => rootRouteImport,
} as any)
const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
id: '/posts_/$postId/deep',
path: '/posts/$postId/deep',
Expand Down Expand Up @@ -127,6 +133,7 @@ export interface FileRoutesByFullPath {
'/layout-b': typeof LayoutLayout2LayoutBRoute
'/api/users/$id': typeof ApiUsersIdRoute
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
'/transition/count/query': typeof TransitionCountQueryRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
Expand All @@ -142,6 +149,7 @@ export interface FileRoutesByTo {
'/layout-b': typeof LayoutLayout2LayoutBRoute
'/api/users/$id': typeof ApiUsersIdRoute
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
'/transition/count/query': typeof TransitionCountQueryRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
Expand All @@ -162,6 +170,7 @@ export interface FileRoutesById {
'/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute
'/api/users/$id': typeof ApiUsersIdRoute
'/posts_/$postId/deep': typeof PostsPostIdDeepRoute
'/transition/count/query': typeof TransitionCountQueryRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
Expand All @@ -181,6 +190,7 @@ export interface FileRouteTypes {
| '/layout-b'
| '/api/users/$id'
| '/posts/$postId/deep'
| '/transition/count/query'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
Expand All @@ -196,6 +206,7 @@ export interface FileRouteTypes {
| '/layout-b'
| '/api/users/$id'
| '/posts/$postId/deep'
| '/transition/count/query'
id:
| '__root__'
| '/'
Expand All @@ -215,6 +226,7 @@ export interface FileRouteTypes {
| '/_layout/_layout-2/layout-b'
| '/api/users/$id'
| '/posts_/$postId/deep'
| '/transition/count/query'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
Expand All @@ -227,6 +239,7 @@ export interface RootRouteChildren {
UsersRoute: typeof UsersRouteWithChildren
ApiUsersRoute: typeof ApiUsersRouteWithChildren
PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute
TransitionCountQueryRoute: typeof TransitionCountQueryRoute
}

declare module '@tanstack/react-router' {
Expand Down Expand Up @@ -322,6 +335,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LayoutLayout2RouteImport
parentRoute: typeof LayoutRoute
}
'/transition/count/query': {
id: '/transition/count/query'
path: '/transition/count/query'
fullPath: '/transition/count/query'
preLoaderRoute: typeof TransitionCountQueryRouteImport
parentRoute: typeof rootRouteImport
}
'/posts_/$postId/deep': {
id: '/posts_/$postId/deep'
path: '/posts/$postId/deep'
Expand Down Expand Up @@ -424,6 +444,7 @@ const rootRouteChildren: RootRouteChildren = {
UsersRoute: UsersRouteWithChildren,
ApiUsersRoute: ApiUsersRouteWithChildren,
PostsPostIdDeepRoute: PostsPostIdDeepRoute,
TransitionCountQueryRoute: TransitionCountQueryRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Link, createFileRoute } from '@tanstack/react-router'
import { Suspense } from 'react'
import { queryOptions, useQuery } from '@tanstack/react-query'
import { z } from 'zod'

const searchSchema = z.object({
n: z.number().default(1),
})

const doubleQueryOptions = (n: number) =>
queryOptions({
queryKey: ['transition-double', n],
queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return { n, double: n * 2 }
},
placeholderData: (oldData) => oldData,
})

export const Route = createFileRoute('/transition/count/query')({
validateSearch: searchSchema,
loader: ({ context: { queryClient }, location }) => {
const { n } = searchSchema.parse(location.search)
return queryClient.ensureQueryData(doubleQueryOptions(n))
},
component: TransitionPage,
})

function TransitionPage() {
const search = Route.useSearch()

const doubleQuery = useQuery(doubleQueryOptions(search.n))

return (
<Suspense fallback="Loading...">
<div className="p-2">
<Link
data-testid="increase-button"
className="border bg-gray-50 px-3 py-1"
from="/transition/count/query"
search={(s) => ({ n: s.n + 1 })}
>
Increase
</Link>

<div className="mt-2">
<div data-testid="n-value">n: {doubleQuery.data?.n}</div>
<div data-testid="double-value">
double: {doubleQuery.data?.double}
</div>
</div>
</div>
</Suspense>
)
}
95 changes: 95 additions & 0 deletions e2e/react-start/basic-react-query/tests/transition.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { expect, test } from '@playwright/test'

test('transitions/count/query should keep old values visible during navigation', async ({
page,
}) => {
await page.goto('/transition/count/query')

await expect(page.getByTestId('n-value')).toContainText('n: 1')
await expect(page.getByTestId('double-value')).toContainText('double: 2')

const bodyTexts: Array<string> = []

const pollInterval = setInterval(async () => {
const text = await page
.locator('body')
.textContent()
.catch(() => '')
if (text) bodyTexts.push(text)
}, 50)

// 1 click

page.getByTestId('increase-button').click()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Await the click action to avoid race conditions.

page.getByTestId('increase-button').click() returns a promise. Without await, the test can assert before the click completes, causing flakiness or false positives. Please await the click so Playwright finishes dispatching the event before moving on.

Apply this diff:

-  page.getByTestId('increase-button').click()
+  await page.getByTestId('increase-button').click()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
page.getByTestId('increase-button').click()
await page.getByTestId('increase-button').click()
🤖 Prompt for AI Agents
In e2e/react-start/basic-react-query/tests/transition.spec.ts around line 23,
the test calls page.getByTestId('increase-button').click() without awaiting it;
change this to await the click call so the test waits for the click promise to
resolve before proceeding, preventing race conditions and flakiness.


await expect(page.getByTestId('n-value')).toContainText('n: 1', {
timeout: 2_000,
})
await expect(page.getByTestId('double-value')).toContainText('double: 2', {
timeout: 2_000,
})

await page.waitForTimeout(200)

await expect(page.getByTestId('n-value')).toContainText('n: 2', {
timeout: 2000,
})
await expect(page.getByTestId('double-value')).toContainText('double: 4', {
timeout: 2000,
})

// 2 clicks

// page.getByTestId('increase-button').click()
// page.getByTestId('increase-button').click()

// await expect(page.getByTestId('n-value')).toContainText('n: 2', {
// timeout: 2000,
// })
// await expect(page.getByTestId('double-value')).toContainText('double: 4', {
// timeout: 2000,
// })

// await page.waitForTimeout(200)

// await expect(page.getByTestId('n-value')).toContainText('n: 4', {
// timeout: 2000,
// })
// await expect(page.getByTestId('double-value')).toContainText('double: 8', {
// timeout: 2000,
// })

// // 3 clicks

// page.getByTestId('increase-button').click()
// page.getByTestId('increase-button').click()
// page.getByTestId('increase-button').click()

// await expect(page.getByTestId('n-value')).toContainText('n: 4', {
// timeout: 2000,
// })
// await expect(page.getByTestId('double-value')).toContainText('double: 8', {
// timeout: 2000,
// })

// await page.waitForTimeout(200)

// await expect(page.getByTestId('n-value')).toContainText('n: 7', {
// timeout: 2000,
// })
// await expect(page.getByTestId('double-value')).toContainText('double: 14', {
// timeout: 2000,
// })

clearInterval(pollInterval)

// With proper transitions, old values should remain visible until new ones arrive
const hasLoadingText = bodyTexts.some((text) => text.includes('Loading...'))

if (hasLoadingText) {
throw new Error(
'FAILED: "Loading..." appeared during navigation. ' +
'Solid Router should use transitions to keep old values visible.',
)
}
Comment on lines +89 to +94
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix incorrect framework name in error message.

The error message references "Solid Router" but this test is for React Router/React Start. This is a copy-paste error that would confuse developers if the test fails.

Apply this diff:

   if (hasLoadingText) {
     throw new Error(
       'FAILED: "Loading..." appeared during navigation. ' +
-        'Solid Router should use transitions to keep old values visible.',
+        'React Router should use transitions to keep old values visible.',
     )
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (hasLoadingText) {
throw new Error(
'FAILED: "Loading..." appeared during navigation. ' +
'Solid Router should use transitions to keep old values visible.',
)
}
if (hasLoadingText) {
throw new Error(
'FAILED: "Loading..." appeared during navigation. ' +
'React Router should use transitions to keep old values visible.',
)
}
🤖 Prompt for AI Agents
In e2e/react-start/basic-react-query/tests/transition.spec.ts around lines 89 to
94, the error message incorrectly mentions "Solid Router"; update the thrown
Error string to reference "React Router" (or "React Start" if that aligns with
project naming) so it reads something like 'FAILED: "Loading..." appeared during
navigation. React Router should use transitions to keep old values visible.';
ensure punctuation and concatenation remain correct.

})
Loading
Loading