Skip to content

Commit 1b75808

Browse files
test(router, start): query navigation transitions (#5802)
* test(router): test query transitions * ci: apply automated fixes * test start * ci: apply automated fixes * dim double click test --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent edb5365 commit 1b75808

File tree

13 files changed

+504
-34
lines changed

13 files changed

+504
-34
lines changed

e2e/react-router/basic-react-query-file-based/src/routes/transition/count/query.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Link, createFileRoute } from '@tanstack/react-router'
2-
import { Suspense, useMemo } from 'react'
2+
import { Suspense } from 'react'
33
import { queryOptions, useQuery } from '@tanstack/react-query'
44
import { z } from 'zod'
55

@@ -12,7 +12,7 @@ const doubleQueryOptions = (n: number) =>
1212
queryKey: ['transition-double', n],
1313
queryFn: async () => {
1414
await new Promise((resolve) => setTimeout(resolve, 1000))
15-
return n * 2
15+
return { n, double: n * 2 }
1616
},
1717
placeholderData: (oldData) => oldData,
1818
})
@@ -44,8 +44,10 @@ function TransitionPage() {
4444
</Link>
4545

4646
<div className="mt-2">
47-
<div data-testid="n-value">n: {search.n}</div>
48-
<div data-testid="double-value">double: {doubleQuery.data}</div>
47+
<div data-testid="n-value">n: {doubleQuery.data?.n}</div>
48+
<div data-testid="double-value">
49+
double: {doubleQuery.data?.double}
50+
</div>
4951
</div>
5052
</div>
5153
</Suspense>
Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,95 @@
11
import { expect, test } from '@playwright/test'
22

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

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

11-
const bodySnapshots: Array<string> = []
11+
const bodyTexts: Array<string> = []
1212

13-
const interval = setInterval(async () => {
13+
const pollInterval = setInterval(async () => {
1414
const text = await page
1515
.locator('body')
1616
.textContent()
1717
.catch(() => '')
18-
if (text) bodySnapshots.push(text)
18+
if (text) bodyTexts.push(text)
1919
}, 50)
2020

21-
await page.getByTestId('increase-button').click()
21+
// 1 click
22+
23+
page.getByTestId('increase-button').click()
24+
25+
await expect(page.getByTestId('n-value')).toContainText('n: 1', {
26+
timeout: 2_000,
27+
})
28+
await expect(page.getByTestId('double-value')).toContainText('double: 2', {
29+
timeout: 2_000,
30+
})
2231

2332
await page.waitForTimeout(200)
2433

25-
clearInterval(interval)
34+
await expect(page.getByTestId('n-value')).toContainText('n: 2', {
35+
timeout: 2000,
36+
})
37+
await expect(page.getByTestId('double-value')).toContainText('double: 4', {
38+
timeout: 2000,
39+
})
40+
41+
// 2 clicks
42+
43+
page.getByTestId('increase-button').click()
44+
page.getByTestId('increase-button').click()
2645

2746
await expect(page.getByTestId('n-value')).toContainText('n: 2', {
28-
timeout: 2_000,
47+
timeout: 2000,
2948
})
3049
await expect(page.getByTestId('double-value')).toContainText('double: 4', {
31-
timeout: 2_000,
50+
timeout: 2000,
51+
})
52+
53+
await page.waitForTimeout(200)
54+
55+
await expect(page.getByTestId('n-value')).toContainText('n: 4', {
56+
timeout: 2000,
57+
})
58+
await expect(page.getByTestId('double-value')).toContainText('double: 8', {
59+
timeout: 2000,
60+
})
61+
62+
// 3 clicks
63+
64+
page.getByTestId('increase-button').click()
65+
page.getByTestId('increase-button').click()
66+
page.getByTestId('increase-button').click()
67+
68+
await expect(page.getByTestId('n-value')).toContainText('n: 4', {
69+
timeout: 2000,
3270
})
71+
await expect(page.getByTestId('double-value')).toContainText('double: 8', {
72+
timeout: 2000,
73+
})
74+
75+
await page.waitForTimeout(200)
76+
77+
await expect(page.getByTestId('n-value')).toContainText('n: 7', {
78+
timeout: 2000,
79+
})
80+
await expect(page.getByTestId('double-value')).toContainText('double: 14', {
81+
timeout: 2000,
82+
})
83+
84+
clearInterval(pollInterval)
3385

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

36-
expect(sawLoading).toBeFalsy()
89+
if (hasLoadingText) {
90+
throw new Error(
91+
'FAILED: "Loading..." appeared during navigation. ' +
92+
'Solid Router should use transitions to keep old values visible.',
93+
)
94+
}
3795
})

e2e/react-start/basic-react-query/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"react-dom": "^19.0.0",
2222
"redaxios": "^0.5.1",
2323
"tailwind-merge": "^2.6.0",
24-
"vite": "^7.1.7"
24+
"vite": "^7.1.7",
25+
"zod": "^4.1.12"
2526
},
2627
"devDependencies": {
2728
"@playwright/test": "^1.50.1",

e2e/react-start/basic-react-query/src/routeTree.gen.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
2222
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
2323
import { Route as ApiUsersRouteImport } from './routes/api.users'
2424
import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
25+
import { Route as TransitionCountQueryRouteImport } from './routes/transition/count/query'
2526
import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
2627
import { Route as ApiUsersIdRouteImport } from './routes/api/users.$id'
2728
import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
@@ -90,6 +91,11 @@ const LayoutLayout2Route = LayoutLayout2RouteImport.update({
9091
id: '/_layout-2',
9192
getParentRoute: () => LayoutRoute,
9293
} as any)
94+
const TransitionCountQueryRoute = TransitionCountQueryRouteImport.update({
95+
id: '/transition/count/query',
96+
path: '/transition/count/query',
97+
getParentRoute: () => rootRouteImport,
98+
} as any)
9399
const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
94100
id: '/posts_/$postId/deep',
95101
path: '/posts/$postId/deep',
@@ -127,6 +133,7 @@ export interface FileRoutesByFullPath {
127133
'/layout-b': typeof LayoutLayout2LayoutBRoute
128134
'/api/users/$id': typeof ApiUsersIdRoute
129135
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
136+
'/transition/count/query': typeof TransitionCountQueryRoute
130137
}
131138
export interface FileRoutesByTo {
132139
'/': typeof IndexRoute
@@ -142,6 +149,7 @@ export interface FileRoutesByTo {
142149
'/layout-b': typeof LayoutLayout2LayoutBRoute
143150
'/api/users/$id': typeof ApiUsersIdRoute
144151
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
152+
'/transition/count/query': typeof TransitionCountQueryRoute
145153
}
146154
export interface FileRoutesById {
147155
__root__: typeof rootRouteImport
@@ -162,6 +170,7 @@ export interface FileRoutesById {
162170
'/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute
163171
'/api/users/$id': typeof ApiUsersIdRoute
164172
'/posts_/$postId/deep': typeof PostsPostIdDeepRoute
173+
'/transition/count/query': typeof TransitionCountQueryRoute
165174
}
166175
export interface FileRouteTypes {
167176
fileRoutesByFullPath: FileRoutesByFullPath
@@ -181,6 +190,7 @@ export interface FileRouteTypes {
181190
| '/layout-b'
182191
| '/api/users/$id'
183192
| '/posts/$postId/deep'
193+
| '/transition/count/query'
184194
fileRoutesByTo: FileRoutesByTo
185195
to:
186196
| '/'
@@ -196,6 +206,7 @@ export interface FileRouteTypes {
196206
| '/layout-b'
197207
| '/api/users/$id'
198208
| '/posts/$postId/deep'
209+
| '/transition/count/query'
199210
id:
200211
| '__root__'
201212
| '/'
@@ -215,6 +226,7 @@ export interface FileRouteTypes {
215226
| '/_layout/_layout-2/layout-b'
216227
| '/api/users/$id'
217228
| '/posts_/$postId/deep'
229+
| '/transition/count/query'
218230
fileRoutesById: FileRoutesById
219231
}
220232
export interface RootRouteChildren {
@@ -227,6 +239,7 @@ export interface RootRouteChildren {
227239
UsersRoute: typeof UsersRouteWithChildren
228240
ApiUsersRoute: typeof ApiUsersRouteWithChildren
229241
PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute
242+
TransitionCountQueryRoute: typeof TransitionCountQueryRoute
230243
}
231244

232245
declare module '@tanstack/react-router' {
@@ -322,6 +335,13 @@ declare module '@tanstack/react-router' {
322335
preLoaderRoute: typeof LayoutLayout2RouteImport
323336
parentRoute: typeof LayoutRoute
324337
}
338+
'/transition/count/query': {
339+
id: '/transition/count/query'
340+
path: '/transition/count/query'
341+
fullPath: '/transition/count/query'
342+
preLoaderRoute: typeof TransitionCountQueryRouteImport
343+
parentRoute: typeof rootRouteImport
344+
}
325345
'/posts_/$postId/deep': {
326346
id: '/posts_/$postId/deep'
327347
path: '/posts/$postId/deep'
@@ -424,6 +444,7 @@ const rootRouteChildren: RootRouteChildren = {
424444
UsersRoute: UsersRouteWithChildren,
425445
ApiUsersRoute: ApiUsersRouteWithChildren,
426446
PostsPostIdDeepRoute: PostsPostIdDeepRoute,
447+
TransitionCountQueryRoute: TransitionCountQueryRoute,
427448
}
428449
export const routeTree = rootRouteImport
429450
._addFileChildren(rootRouteChildren)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Link, createFileRoute } from '@tanstack/react-router'
2+
import { Suspense } from 'react'
3+
import { queryOptions, useQuery } from '@tanstack/react-query'
4+
import { z } from 'zod'
5+
6+
const searchSchema = z.object({
7+
n: z.number().default(1),
8+
})
9+
10+
const doubleQueryOptions = (n: number) =>
11+
queryOptions({
12+
queryKey: ['transition-double', n],
13+
queryFn: async () => {
14+
await new Promise((resolve) => setTimeout(resolve, 1000))
15+
return { n, double: n * 2 }
16+
},
17+
placeholderData: (oldData) => oldData,
18+
})
19+
20+
export const Route = createFileRoute('/transition/count/query')({
21+
validateSearch: searchSchema,
22+
loader: ({ context: { queryClient }, location }) => {
23+
const { n } = searchSchema.parse(location.search)
24+
return queryClient.ensureQueryData(doubleQueryOptions(n))
25+
},
26+
component: TransitionPage,
27+
})
28+
29+
function TransitionPage() {
30+
const search = Route.useSearch()
31+
32+
const doubleQuery = useQuery(doubleQueryOptions(search.n))
33+
34+
return (
35+
<Suspense fallback="Loading...">
36+
<div className="p-2">
37+
<Link
38+
data-testid="increase-button"
39+
className="border bg-gray-50 px-3 py-1"
40+
from="/transition/count/query"
41+
search={(s) => ({ n: s.n + 1 })}
42+
>
43+
Increase
44+
</Link>
45+
46+
<div className="mt-2">
47+
<div data-testid="n-value">n: {doubleQuery.data?.n}</div>
48+
<div data-testid="double-value">
49+
double: {doubleQuery.data?.double}
50+
</div>
51+
</div>
52+
</div>
53+
</Suspense>
54+
)
55+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { expect, test } from '@playwright/test'
2+
3+
test('transitions/count/query should keep old values visible during navigation', async ({
4+
page,
5+
}) => {
6+
await page.goto('/transition/count/query')
7+
8+
await expect(page.getByTestId('n-value')).toContainText('n: 1')
9+
await expect(page.getByTestId('double-value')).toContainText('double: 2')
10+
11+
const bodyTexts: Array<string> = []
12+
13+
const pollInterval = setInterval(async () => {
14+
const text = await page
15+
.locator('body')
16+
.textContent()
17+
.catch(() => '')
18+
if (text) bodyTexts.push(text)
19+
}, 50)
20+
21+
// 1 click
22+
23+
page.getByTestId('increase-button').click()
24+
25+
await expect(page.getByTestId('n-value')).toContainText('n: 1', {
26+
timeout: 2_000,
27+
})
28+
await expect(page.getByTestId('double-value')).toContainText('double: 2', {
29+
timeout: 2_000,
30+
})
31+
32+
await page.waitForTimeout(200)
33+
34+
await expect(page.getByTestId('n-value')).toContainText('n: 2', {
35+
timeout: 2000,
36+
})
37+
await expect(page.getByTestId('double-value')).toContainText('double: 4', {
38+
timeout: 2000,
39+
})
40+
41+
// 2 clicks
42+
43+
// page.getByTestId('increase-button').click()
44+
// page.getByTestId('increase-button').click()
45+
46+
// await expect(page.getByTestId('n-value')).toContainText('n: 2', {
47+
// timeout: 2000,
48+
// })
49+
// await expect(page.getByTestId('double-value')).toContainText('double: 4', {
50+
// timeout: 2000,
51+
// })
52+
53+
// await page.waitForTimeout(200)
54+
55+
// await expect(page.getByTestId('n-value')).toContainText('n: 4', {
56+
// timeout: 2000,
57+
// })
58+
// await expect(page.getByTestId('double-value')).toContainText('double: 8', {
59+
// timeout: 2000,
60+
// })
61+
62+
// // 3 clicks
63+
64+
// page.getByTestId('increase-button').click()
65+
// page.getByTestId('increase-button').click()
66+
// page.getByTestId('increase-button').click()
67+
68+
// await expect(page.getByTestId('n-value')).toContainText('n: 4', {
69+
// timeout: 2000,
70+
// })
71+
// await expect(page.getByTestId('double-value')).toContainText('double: 8', {
72+
// timeout: 2000,
73+
// })
74+
75+
// await page.waitForTimeout(200)
76+
77+
// await expect(page.getByTestId('n-value')).toContainText('n: 7', {
78+
// timeout: 2000,
79+
// })
80+
// await expect(page.getByTestId('double-value')).toContainText('double: 14', {
81+
// timeout: 2000,
82+
// })
83+
84+
clearInterval(pollInterval)
85+
86+
// With proper transitions, old values should remain visible until new ones arrive
87+
const hasLoadingText = bodyTexts.some((text) => text.includes('Loading...'))
88+
89+
if (hasLoadingText) {
90+
throw new Error(
91+
'FAILED: "Loading..." appeared during navigation. ' +
92+
'Solid Router should use transitions to keep old values visible.',
93+
)
94+
}
95+
})

0 commit comments

Comments
 (0)