Skip to content

Commit ef2757f

Browse files
test(solid-start): manual suspense boundaries transition on navigation (#5638)
* test(solid-start): manual suspense boundaries transition on navigation * add nested tests * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 67acbde commit ef2757f

File tree

6 files changed

+241
-0
lines changed

6 files changed

+241
-0
lines changed

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import { Route as rootRouteImport } from './routes/__root'
1212
import { Route as UsersRouteImport } from './routes/users'
13+
import { Route as SuspenseTransitionRouteImport } from './routes/suspense-transition'
1314
import { Route as RedirectRouteImport } from './routes/redirect'
1415
import { Route as PostsRouteImport } from './routes/posts'
1516
import { Route as DeferredRouteImport } from './routes/deferred'
@@ -31,6 +32,11 @@ const UsersRoute = UsersRouteImport.update({
3132
path: '/users',
3233
getParentRoute: () => rootRouteImport,
3334
} as any)
35+
const SuspenseTransitionRoute = SuspenseTransitionRouteImport.update({
36+
id: '/suspense-transition',
37+
path: '/suspense-transition',
38+
getParentRoute: () => rootRouteImport,
39+
} as any)
3440
const RedirectRoute = RedirectRouteImport.update({
3541
id: '/redirect',
3642
path: '/redirect',
@@ -110,6 +116,7 @@ export interface FileRoutesByFullPath {
110116
'/deferred': typeof DeferredRoute
111117
'/posts': typeof PostsRouteWithChildren
112118
'/redirect': typeof RedirectRoute
119+
'/suspense-transition': typeof SuspenseTransitionRoute
113120
'/users': typeof UsersRouteWithChildren
114121
'/api/users': typeof ApiUsersRouteWithChildren
115122
'/posts/$postId': typeof PostsPostIdRoute
@@ -125,6 +132,7 @@ export interface FileRoutesByTo {
125132
'/': typeof IndexRoute
126133
'/deferred': typeof DeferredRoute
127134
'/redirect': typeof RedirectRoute
135+
'/suspense-transition': typeof SuspenseTransitionRoute
128136
'/api/users': typeof ApiUsersRouteWithChildren
129137
'/posts/$postId': typeof PostsPostIdRoute
130138
'/users/$userId': typeof UsersUserIdRoute
@@ -142,6 +150,7 @@ export interface FileRoutesById {
142150
'/deferred': typeof DeferredRoute
143151
'/posts': typeof PostsRouteWithChildren
144152
'/redirect': typeof RedirectRoute
153+
'/suspense-transition': typeof SuspenseTransitionRoute
145154
'/users': typeof UsersRouteWithChildren
146155
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
147156
'/api/users': typeof ApiUsersRouteWithChildren
@@ -161,6 +170,7 @@ export interface FileRouteTypes {
161170
| '/deferred'
162171
| '/posts'
163172
| '/redirect'
173+
| '/suspense-transition'
164174
| '/users'
165175
| '/api/users'
166176
| '/posts/$postId'
@@ -176,6 +186,7 @@ export interface FileRouteTypes {
176186
| '/'
177187
| '/deferred'
178188
| '/redirect'
189+
| '/suspense-transition'
179190
| '/api/users'
180191
| '/posts/$postId'
181192
| '/users/$userId'
@@ -192,6 +203,7 @@ export interface FileRouteTypes {
192203
| '/deferred'
193204
| '/posts'
194205
| '/redirect'
206+
| '/suspense-transition'
195207
| '/users'
196208
| '/_layout/_layout-2'
197209
| '/api/users'
@@ -211,6 +223,7 @@ export interface RootRouteChildren {
211223
DeferredRoute: typeof DeferredRoute
212224
PostsRoute: typeof PostsRouteWithChildren
213225
RedirectRoute: typeof RedirectRoute
226+
SuspenseTransitionRoute: typeof SuspenseTransitionRoute
214227
UsersRoute: typeof UsersRouteWithChildren
215228
ApiUsersRoute: typeof ApiUsersRouteWithChildren
216229
PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute
@@ -225,6 +238,13 @@ declare module '@tanstack/react-router' {
225238
preLoaderRoute: typeof UsersRouteImport
226239
parentRoute: typeof rootRouteImport
227240
}
241+
'/suspense-transition': {
242+
id: '/suspense-transition'
243+
path: '/suspense-transition'
244+
fullPath: '/suspense-transition'
245+
preLoaderRoute: typeof SuspenseTransitionRouteImport
246+
parentRoute: typeof rootRouteImport
247+
}
228248
'/redirect': {
229249
id: '/redirect'
230250
path: '/redirect'
@@ -400,6 +420,7 @@ const rootRouteChildren: RootRouteChildren = {
400420
DeferredRoute: DeferredRoute,
401421
PostsRoute: PostsRouteWithChildren,
402422
RedirectRoute: RedirectRoute,
423+
SuspenseTransitionRoute: SuspenseTransitionRoute,
403424
UsersRoute: UsersRouteWithChildren,
404425
ApiUsersRoute: ApiUsersRouteWithChildren,
405426
PostsPostIdDeepRoute: PostsPostIdDeepRoute,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { queryOptions, useQuery } from '@tanstack/react-query'
2+
import { Link, createFileRoute } from '@tanstack/react-router'
3+
import { Suspense } from 'react'
4+
5+
const doubleQueryOptions = (n: number) =>
6+
queryOptions({
7+
queryKey: ['double', n],
8+
queryFn: async () => {
9+
// Add a delay to make the transition observable
10+
await new Promise((r) => setTimeout(r, 500))
11+
return n * 2
12+
},
13+
})
14+
15+
export const Route = createFileRoute('/suspense-transition')({
16+
validateSearch: (search: { n?: number }) => ({ n: search.n ?? 1 }),
17+
component: SuspenseTransitionComponent,
18+
ssr: false, // Disable SSR to avoid suspense issues during initial load
19+
})
20+
21+
function SuspenseTransitionComponent() {
22+
return (
23+
<div className="p-2">
24+
<h1 data-testid="suspense-transition-title">Suspense Transition Test</h1>
25+
26+
<div className="flex gap-2 my-4">
27+
<Link
28+
data-testid="increase-button"
29+
className="border bg-gray-50 px-3 py-1"
30+
from="/suspense-transition"
31+
search={(s) => ({ n: s.n + 1 })}
32+
>
33+
Increase
34+
</Link>
35+
</div>
36+
37+
<Result />
38+
</div>
39+
)
40+
}
41+
42+
function Result() {
43+
const search = Route.useSearch()
44+
const doubleQuery = useQuery(doubleQueryOptions(search.n))
45+
46+
return (
47+
<div className="mt-2 border p-4">
48+
{/* This manual Suspense boundary should transition, not immediately show fallback */}
49+
<Suspense
50+
fallback={<div data-testid="suspense-fallback">Loading...</div>}
51+
>
52+
<div data-testid="suspense-content">
53+
<div>
54+
n: <span data-testid="n-value">{search.n}</span>
55+
</div>
56+
<div>
57+
double: <span data-testid="double-value">{doubleQuery.data}</span>
58+
</div>
59+
</div>
60+
</Suspense>
61+
</div>
62+
)
63+
}

e2e/react-start/basic-react-query/tests/app.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,30 @@ test('Navigating to a not-found route', async ({ page }) => {
3434
await page.getByRole('link', { name: 'Start Over' }).click()
3535
await expect(page.getByRole('heading')).toContainText('Welcome Home!')
3636
})
37+
38+
test('Manual Suspense boundaries should transition on navigation', async ({
39+
page,
40+
}) => {
41+
// Navigate to the suspense transition test page
42+
await page.goto('/suspense-transition')
43+
44+
// Wait for initial content to load
45+
await expect(page.getByTestId('n-value')).toHaveText('1')
46+
await expect(page.getByTestId('double-value')).toHaveText('2')
47+
48+
// Click the increase button to trigger navigation with search params change
49+
await page.getByTestId('increase-button').click()
50+
51+
// During the transition, the old content should remain visible
52+
// and the fallback should NOT be shown
53+
await expect(page.getByTestId('suspense-fallback')).not.toBeVisible({
54+
timeout: 100,
55+
})
56+
57+
// The old content should still be visible during transition
58+
await expect(page.getByTestId('suspense-content')).toBeVisible()
59+
60+
// After transition completes, new content should be visible
61+
await expect(page.getByTestId('n-value')).toHaveText('2')
62+
await expect(page.getByTestId('double-value')).toHaveText('4')
63+
})

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import { Route as rootRouteImport } from './routes/__root'
1212
import { Route as UsersRouteImport } from './routes/users'
13+
import { Route as SuspenseTransitionRouteImport } from './routes/suspense-transition'
1314
import { Route as PostsRouteImport } from './routes/posts'
1415
import { Route as DeferredRouteImport } from './routes/deferred'
1516
import { Route as IndexRouteImport } from './routes/index'
@@ -26,6 +27,11 @@ const UsersRoute = UsersRouteImport.update({
2627
path: '/users',
2728
getParentRoute: () => rootRouteImport,
2829
} as any)
30+
const SuspenseTransitionRoute = SuspenseTransitionRouteImport.update({
31+
id: '/suspense-transition',
32+
path: '/suspense-transition',
33+
getParentRoute: () => rootRouteImport,
34+
} as any)
2935
const PostsRoute = PostsRouteImport.update({
3036
id: '/posts',
3137
path: '/posts',
@@ -81,6 +87,7 @@ export interface FileRoutesByFullPath {
8187
'/': typeof IndexRoute
8288
'/deferred': typeof DeferredRoute
8389
'/posts': typeof PostsRouteWithChildren
90+
'/suspense-transition': typeof SuspenseTransitionRoute
8491
'/users': typeof UsersRouteWithChildren
8592
'/api/users': typeof ApiUsersRouteWithChildren
8693
'/posts/$postId': typeof PostsPostIdRoute
@@ -93,6 +100,7 @@ export interface FileRoutesByFullPath {
93100
export interface FileRoutesByTo {
94101
'/': typeof IndexRoute
95102
'/deferred': typeof DeferredRoute
103+
'/suspense-transition': typeof SuspenseTransitionRoute
96104
'/api/users': typeof ApiUsersRouteWithChildren
97105
'/posts/$postId': typeof PostsPostIdRoute
98106
'/users/$userId': typeof UsersUserIdRoute
@@ -106,6 +114,7 @@ export interface FileRoutesById {
106114
'/': typeof IndexRoute
107115
'/deferred': typeof DeferredRoute
108116
'/posts': typeof PostsRouteWithChildren
117+
'/suspense-transition': typeof SuspenseTransitionRoute
109118
'/users': typeof UsersRouteWithChildren
110119
'/api/users': typeof ApiUsersRouteWithChildren
111120
'/posts/$postId': typeof PostsPostIdRoute
@@ -121,6 +130,7 @@ export interface FileRouteTypes {
121130
| '/'
122131
| '/deferred'
123132
| '/posts'
133+
| '/suspense-transition'
124134
| '/users'
125135
| '/api/users'
126136
| '/posts/$postId'
@@ -133,6 +143,7 @@ export interface FileRouteTypes {
133143
to:
134144
| '/'
135145
| '/deferred'
146+
| '/suspense-transition'
136147
| '/api/users'
137148
| '/posts/$postId'
138149
| '/users/$userId'
@@ -145,6 +156,7 @@ export interface FileRouteTypes {
145156
| '/'
146157
| '/deferred'
147158
| '/posts'
159+
| '/suspense-transition'
148160
| '/users'
149161
| '/api/users'
150162
| '/posts/$postId'
@@ -159,6 +171,7 @@ export interface RootRouteChildren {
159171
IndexRoute: typeof IndexRoute
160172
DeferredRoute: typeof DeferredRoute
161173
PostsRoute: typeof PostsRouteWithChildren
174+
SuspenseTransitionRoute: typeof SuspenseTransitionRoute
162175
UsersRoute: typeof UsersRouteWithChildren
163176
ApiUsersRoute: typeof ApiUsersRouteWithChildren
164177
PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute
@@ -173,6 +186,13 @@ declare module '@tanstack/solid-router' {
173186
preLoaderRoute: typeof UsersRouteImport
174187
parentRoute: typeof rootRouteImport
175188
}
189+
'/suspense-transition': {
190+
id: '/suspense-transition'
191+
path: '/suspense-transition'
192+
fullPath: '/suspense-transition'
193+
preLoaderRoute: typeof SuspenseTransitionRouteImport
194+
parentRoute: typeof rootRouteImport
195+
}
176196
'/posts': {
177197
id: '/posts'
178198
path: '/posts'
@@ -286,6 +306,7 @@ const rootRouteChildren: RootRouteChildren = {
286306
IndexRoute: IndexRoute,
287307
DeferredRoute: DeferredRoute,
288308
PostsRoute: PostsRouteWithChildren,
309+
SuspenseTransitionRoute: SuspenseTransitionRoute,
289310
UsersRoute: UsersRouteWithChildren,
290311
ApiUsersRoute: ApiUsersRouteWithChildren,
291312
PostsPostIdDeepRoute: PostsPostIdDeepRoute,
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { queryOptions, useQuery } from '@tanstack/solid-query'
2+
import { Link, createFileRoute } from '@tanstack/solid-router'
3+
import { Suspense } from 'solid-js'
4+
5+
const doubleQueryOptions = (n: number) =>
6+
queryOptions({
7+
queryKey: ['double', n],
8+
queryFn: async () => {
9+
// Add a delay to make the transition observable
10+
await new Promise((r) => setTimeout(r, 500))
11+
return n * 2
12+
},
13+
placeholderData: (previousData) => previousData,
14+
})
15+
16+
export const Route = createFileRoute('/suspense-transition')({
17+
validateSearch: (search: { n?: number }) => ({ n: search.n ?? 1 }),
18+
component: SuspenseTransitionComponent,
19+
ssr: false, // Disable SSR to avoid suspense issues during initial load
20+
})
21+
22+
function SuspenseTransitionComponent() {
23+
return (
24+
<div class="p-2">
25+
<h1 data-testid="suspense-transition-title">Suspense Transition Test</h1>
26+
27+
<div class="flex gap-2 my-4">
28+
<Link
29+
data-testid="increase-button"
30+
class="border bg-gray-50 px-3 py-1"
31+
from="/suspense-transition"
32+
search={(s) => ({ n: s.n + 1 })}
33+
>
34+
Increase
35+
</Link>
36+
</div>
37+
38+
<Result />
39+
</div>
40+
)
41+
}
42+
43+
function Result() {
44+
const search = Route.useSearch()
45+
const doubleQuery = useQuery(() => doubleQueryOptions(search().n))
46+
47+
return (
48+
<div class="mt-2 border p-4">
49+
{/* This manual Suspense boundary should transition, not immediately show fallback */}
50+
<Suspense
51+
fallback={<div data-testid="suspense-fallback">Loading...</div>}
52+
>
53+
<div data-testid="suspense-content">
54+
<div>
55+
n: <span data-testid="n-value">{search().n}</span>
56+
</div>
57+
<div>
58+
double: <span data-testid="double-value">{doubleQuery.data}</span>
59+
</div>
60+
</div>
61+
</Suspense>
62+
</div>
63+
)
64+
}

0 commit comments

Comments
 (0)