Skip to content

Commit cf623b9

Browse files
yanghuidongclaude
andcommitted
test(solid-query): add e2e example for head() async loader bug
Adds test routes demonstrating the head() re-execution fix: - /test-head/article/$id - auth-gated article with dynamic title - /test-head/dashboard - simple dashboard for navigation - /fake-login - simulates login with localStorage Testing flow: 1. Visit /test-head/article/123 (unauthenticated) → Shows "Article Not Found" title & content 2. Click login link → simulate login → redirects to dashboard 3. Press browser BACK button → Article content loads correctly → Page title updates from stale to "Article 123 Title" → Console shows head() executing twice (non-blocking fix) Note: fake-auth.ts uses localStorage for auth state 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
1 parent 1c656a5 commit cf623b9

File tree

5 files changed

+250
-0
lines changed

5 files changed

+250
-0
lines changed

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,19 @@ import { Route as rootRouteImport } from './routes/__root'
1212
import { Route as UsersRouteImport } from './routes/users'
1313
import { Route as SuspenseTransitionRouteImport } from './routes/suspense-transition'
1414
import { Route as PostsRouteImport } from './routes/posts'
15+
import { Route as FakeLoginRouteImport } from './routes/fake-login'
1516
import { Route as DeferredRouteImport } from './routes/deferred'
1617
import { Route as LayoutRouteImport } from './routes/_layout'
1718
import { Route as IndexRouteImport } from './routes/index'
1819
import { Route as UsersIndexRouteImport } from './routes/users.index'
1920
import { Route as PostsIndexRouteImport } from './routes/posts.index'
2021
import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
22+
import { Route as TestHeadDashboardRouteImport } from './routes/test-head/dashboard'
2123
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
2224
import { Route as ApiUsersRouteImport } from './routes/api.users'
2325
import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
2426
import { Route as TransitionCountQueryRouteImport } from './routes/transition/count/query'
27+
import { Route as TestHeadArticleIdRouteImport } from './routes/test-head/article.$id'
2528
import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
2629
import { Route as ApiUsersIdRouteImport } from './routes/api/users.$id'
2730
import { Route as LayoutLayout2LayoutBRouteImport } from './routes/_layout/_layout-2/layout-b'
@@ -42,6 +45,11 @@ const PostsRoute = PostsRouteImport.update({
4245
path: '/posts',
4346
getParentRoute: () => rootRouteImport,
4447
} as any)
48+
const FakeLoginRoute = FakeLoginRouteImport.update({
49+
id: '/fake-login',
50+
path: '/fake-login',
51+
getParentRoute: () => rootRouteImport,
52+
} as any)
4553
const DeferredRoute = DeferredRouteImport.update({
4654
id: '/deferred',
4755
path: '/deferred',
@@ -71,6 +79,11 @@ const UsersUserIdRoute = UsersUserIdRouteImport.update({
7179
path: '/$userId',
7280
getParentRoute: () => UsersRoute,
7381
} as any)
82+
const TestHeadDashboardRoute = TestHeadDashboardRouteImport.update({
83+
id: '/test-head/dashboard',
84+
path: '/test-head/dashboard',
85+
getParentRoute: () => rootRouteImport,
86+
} as any)
7487
const PostsPostIdRoute = PostsPostIdRouteImport.update({
7588
id: '/$postId',
7689
path: '/$postId',
@@ -90,6 +103,11 @@ const TransitionCountQueryRoute = TransitionCountQueryRouteImport.update({
90103
path: '/transition/count/query',
91104
getParentRoute: () => rootRouteImport,
92105
} as any)
106+
const TestHeadArticleIdRoute = TestHeadArticleIdRouteImport.update({
107+
id: '/test-head/article/$id',
108+
path: '/test-head/article/$id',
109+
getParentRoute: () => rootRouteImport,
110+
} as any)
93111
const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
94112
id: '/posts_/$postId/deep',
95113
path: '/posts/$postId/deep',
@@ -114,118 +132,139 @@ const LayoutLayout2LayoutARoute = LayoutLayout2LayoutARouteImport.update({
114132
export interface FileRoutesByFullPath {
115133
'/': typeof IndexRoute
116134
'/deferred': typeof DeferredRoute
135+
'/fake-login': typeof FakeLoginRoute
117136
'/posts': typeof PostsRouteWithChildren
118137
'/suspense-transition': typeof SuspenseTransitionRoute
119138
'/users': typeof UsersRouteWithChildren
120139
'/api/users': typeof ApiUsersRouteWithChildren
121140
'/posts/$postId': typeof PostsPostIdRoute
141+
'/test-head/dashboard': typeof TestHeadDashboardRoute
122142
'/users/$userId': typeof UsersUserIdRoute
123143
'/posts/': typeof PostsIndexRoute
124144
'/users/': typeof UsersIndexRoute
125145
'/layout-a': typeof LayoutLayout2LayoutARoute
126146
'/layout-b': typeof LayoutLayout2LayoutBRoute
127147
'/api/users/$id': typeof ApiUsersIdRoute
128148
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
149+
'/test-head/article/$id': typeof TestHeadArticleIdRoute
129150
'/transition/count/query': typeof TransitionCountQueryRoute
130151
}
131152
export interface FileRoutesByTo {
132153
'/': typeof IndexRoute
133154
'/deferred': typeof DeferredRoute
155+
'/fake-login': typeof FakeLoginRoute
134156
'/suspense-transition': typeof SuspenseTransitionRoute
135157
'/api/users': typeof ApiUsersRouteWithChildren
136158
'/posts/$postId': typeof PostsPostIdRoute
159+
'/test-head/dashboard': typeof TestHeadDashboardRoute
137160
'/users/$userId': typeof UsersUserIdRoute
138161
'/posts': typeof PostsIndexRoute
139162
'/users': typeof UsersIndexRoute
140163
'/layout-a': typeof LayoutLayout2LayoutARoute
141164
'/layout-b': typeof LayoutLayout2LayoutBRoute
142165
'/api/users/$id': typeof ApiUsersIdRoute
143166
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
167+
'/test-head/article/$id': typeof TestHeadArticleIdRoute
144168
'/transition/count/query': typeof TransitionCountQueryRoute
145169
}
146170
export interface FileRoutesById {
147171
__root__: typeof rootRouteImport
148172
'/': typeof IndexRoute
149173
'/_layout': typeof LayoutRouteWithChildren
150174
'/deferred': typeof DeferredRoute
175+
'/fake-login': typeof FakeLoginRoute
151176
'/posts': typeof PostsRouteWithChildren
152177
'/suspense-transition': typeof SuspenseTransitionRoute
153178
'/users': typeof UsersRouteWithChildren
154179
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
155180
'/api/users': typeof ApiUsersRouteWithChildren
156181
'/posts/$postId': typeof PostsPostIdRoute
182+
'/test-head/dashboard': typeof TestHeadDashboardRoute
157183
'/users/$userId': typeof UsersUserIdRoute
158184
'/posts/': typeof PostsIndexRoute
159185
'/users/': typeof UsersIndexRoute
160186
'/_layout/_layout-2/layout-a': typeof LayoutLayout2LayoutARoute
161187
'/_layout/_layout-2/layout-b': typeof LayoutLayout2LayoutBRoute
162188
'/api/users/$id': typeof ApiUsersIdRoute
163189
'/posts_/$postId/deep': typeof PostsPostIdDeepRoute
190+
'/test-head/article/$id': typeof TestHeadArticleIdRoute
164191
'/transition/count/query': typeof TransitionCountQueryRoute
165192
}
166193
export interface FileRouteTypes {
167194
fileRoutesByFullPath: FileRoutesByFullPath
168195
fullPaths:
169196
| '/'
170197
| '/deferred'
198+
| '/fake-login'
171199
| '/posts'
172200
| '/suspense-transition'
173201
| '/users'
174202
| '/api/users'
175203
| '/posts/$postId'
204+
| '/test-head/dashboard'
176205
| '/users/$userId'
177206
| '/posts/'
178207
| '/users/'
179208
| '/layout-a'
180209
| '/layout-b'
181210
| '/api/users/$id'
182211
| '/posts/$postId/deep'
212+
| '/test-head/article/$id'
183213
| '/transition/count/query'
184214
fileRoutesByTo: FileRoutesByTo
185215
to:
186216
| '/'
187217
| '/deferred'
218+
| '/fake-login'
188219
| '/suspense-transition'
189220
| '/api/users'
190221
| '/posts/$postId'
222+
| '/test-head/dashboard'
191223
| '/users/$userId'
192224
| '/posts'
193225
| '/users'
194226
| '/layout-a'
195227
| '/layout-b'
196228
| '/api/users/$id'
197229
| '/posts/$postId/deep'
230+
| '/test-head/article/$id'
198231
| '/transition/count/query'
199232
id:
200233
| '__root__'
201234
| '/'
202235
| '/_layout'
203236
| '/deferred'
237+
| '/fake-login'
204238
| '/posts'
205239
| '/suspense-transition'
206240
| '/users'
207241
| '/_layout/_layout-2'
208242
| '/api/users'
209243
| '/posts/$postId'
244+
| '/test-head/dashboard'
210245
| '/users/$userId'
211246
| '/posts/'
212247
| '/users/'
213248
| '/_layout/_layout-2/layout-a'
214249
| '/_layout/_layout-2/layout-b'
215250
| '/api/users/$id'
216251
| '/posts_/$postId/deep'
252+
| '/test-head/article/$id'
217253
| '/transition/count/query'
218254
fileRoutesById: FileRoutesById
219255
}
220256
export interface RootRouteChildren {
221257
IndexRoute: typeof IndexRoute
222258
LayoutRoute: typeof LayoutRouteWithChildren
223259
DeferredRoute: typeof DeferredRoute
260+
FakeLoginRoute: typeof FakeLoginRoute
224261
PostsRoute: typeof PostsRouteWithChildren
225262
SuspenseTransitionRoute: typeof SuspenseTransitionRoute
226263
UsersRoute: typeof UsersRouteWithChildren
227264
ApiUsersRoute: typeof ApiUsersRouteWithChildren
265+
TestHeadDashboardRoute: typeof TestHeadDashboardRoute
228266
PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute
267+
TestHeadArticleIdRoute: typeof TestHeadArticleIdRoute
229268
TransitionCountQueryRoute: typeof TransitionCountQueryRoute
230269
}
231270

@@ -252,6 +291,13 @@ declare module '@tanstack/solid-router' {
252291
preLoaderRoute: typeof PostsRouteImport
253292
parentRoute: typeof rootRouteImport
254293
}
294+
'/fake-login': {
295+
id: '/fake-login'
296+
path: '/fake-login'
297+
fullPath: '/fake-login'
298+
preLoaderRoute: typeof FakeLoginRouteImport
299+
parentRoute: typeof rootRouteImport
300+
}
255301
'/deferred': {
256302
id: '/deferred'
257303
path: '/deferred'
@@ -294,6 +340,13 @@ declare module '@tanstack/solid-router' {
294340
preLoaderRoute: typeof UsersUserIdRouteImport
295341
parentRoute: typeof UsersRoute
296342
}
343+
'/test-head/dashboard': {
344+
id: '/test-head/dashboard'
345+
path: '/test-head/dashboard'
346+
fullPath: '/test-head/dashboard'
347+
preLoaderRoute: typeof TestHeadDashboardRouteImport
348+
parentRoute: typeof rootRouteImport
349+
}
297350
'/posts/$postId': {
298351
id: '/posts/$postId'
299352
path: '/$postId'
@@ -322,6 +375,13 @@ declare module '@tanstack/solid-router' {
322375
preLoaderRoute: typeof TransitionCountQueryRouteImport
323376
parentRoute: typeof rootRouteImport
324377
}
378+
'/test-head/article/$id': {
379+
id: '/test-head/article/$id'
380+
path: '/test-head/article/$id'
381+
fullPath: '/test-head/article/$id'
382+
preLoaderRoute: typeof TestHeadArticleIdRouteImport
383+
parentRoute: typeof rootRouteImport
384+
}
325385
'/posts_/$postId/deep': {
326386
id: '/posts_/$postId/deep'
327387
path: '/posts/$postId/deep'
@@ -418,11 +478,14 @@ const rootRouteChildren: RootRouteChildren = {
418478
IndexRoute: IndexRoute,
419479
LayoutRoute: LayoutRouteWithChildren,
420480
DeferredRoute: DeferredRoute,
481+
FakeLoginRoute: FakeLoginRoute,
421482
PostsRoute: PostsRouteWithChildren,
422483
SuspenseTransitionRoute: SuspenseTransitionRoute,
423484
UsersRoute: UsersRouteWithChildren,
424485
ApiUsersRoute: ApiUsersRouteWithChildren,
486+
TestHeadDashboardRoute: TestHeadDashboardRoute,
425487
PostsPostIdDeepRoute: PostsPostIdDeepRoute,
488+
TestHeadArticleIdRoute: TestHeadArticleIdRoute,
426489
TransitionCountQueryRoute: TransitionCountQueryRoute,
427490
}
428491
export const routeTree = rootRouteImport
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { useQueryClient } from '@tanstack/solid-query'
2+
import { createFileRoute, useNavigate } from '@tanstack/solid-router'
3+
import { authQy } from '~/utils/fake-auth'
4+
5+
export const Route = createFileRoute('/fake-login')({
6+
ssr: false,
7+
head: () => ({
8+
meta: [{ title: 'Login' }],
9+
}),
10+
component: LoginPage,
11+
})
12+
13+
function LoginPage() {
14+
const navigate = useNavigate()
15+
const queryClient = useQueryClient()
16+
17+
const handleLogin = () => {
18+
localStorage.setItem('auth', 'true')
19+
20+
// Critical: Invalidate auth query to trigger refetch
21+
queryClient.invalidateQueries({ queryKey: authQy.queryKey })
22+
23+
// Navigate to dashboard, REPLACING login in history
24+
navigate({ to: '/test-head/dashboard', replace: true })
25+
}
26+
27+
return (
28+
<div class="p-4" data-testid="login-page">
29+
<h1 class="text-2xl font-bold" data-testid="login-title">
30+
Login Page
31+
</h1>
32+
<p class="mt-4">Click below to simulate login</p>
33+
<button
34+
type="button"
35+
onClick={handleLogin}
36+
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
37+
data-testid="login-button"
38+
>
39+
Simulate Login →
40+
</button>
41+
</div>
42+
)
43+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { useQuery } from '@tanstack/solid-query'
2+
import { createFileRoute, Link } from '@tanstack/solid-router'
3+
import { Show } from 'solid-js'
4+
import { authQy, isAuthed } from '~/utils/fake-auth'
5+
6+
// Simulate fetching article - returns null if not authenticated
7+
const fetchArticle = async (id: string) => {
8+
// Simulate API call delay
9+
await new Promise((resolve) => setTimeout(resolve, 200))
10+
11+
const isLoggedIn = isAuthed()
12+
13+
if (!isLoggedIn) {
14+
return null
15+
}
16+
17+
return {
18+
title: `Article ${id} Title`,
19+
content: `This is the content of article ${id}. Lorem ipsum dolor sit amet, consectetur adipiscing elit.`,
20+
}
21+
}
22+
23+
export const Route = createFileRoute('/test-head/article/$id')({
24+
ssr: false,
25+
loader: async ({ params }) => {
26+
const data = await fetchArticle(params.id)
27+
return data
28+
},
29+
30+
head: ({ loaderData }) => {
31+
const title = loaderData?.title || 'Article Not Found'
32+
console.log('[!] head function: title =', title)
33+
return {
34+
meta: [{ title }],
35+
}
36+
},
37+
38+
component: ArticlePage,
39+
})
40+
41+
function ArticlePage() {
42+
const data = Route.useLoaderData()
43+
const authQuery = useQuery(() => authQy)
44+
45+
return (
46+
<Show
47+
when={authQuery.data === true}
48+
fallback={
49+
<div class="p-4" data-testid="article-not-found">
50+
<h1 class="text-2xl font-bold text-red-600">Article not found</h1>
51+
<p class="mt-2">You need to be authenticated to view this article.</p>
52+
<div class="mt-4">
53+
<Link
54+
to="/fake-login"
55+
class="inline-block px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
56+
data-testid="go-to-login-link"
57+
>
58+
Go to Login →
59+
</Link>
60+
</div>
61+
</div>
62+
}
63+
>
64+
<div class="p-4" data-testid="article-content">
65+
<h1 class="text-2xl font-bold" data-testid="article-title">
66+
{data()?.title}
67+
</h1>
68+
<p class="mt-4">{data()?.content}</p>
69+
70+
<div class="mt-4 space-x-2">
71+
<button
72+
type="button"
73+
onClick={() => {
74+
localStorage.removeItem('auth')
75+
window.location.reload()
76+
}}
77+
class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
78+
data-testid="logout-button"
79+
>
80+
Simulate Logout
81+
</button>
82+
</div>
83+
</div>
84+
</Show>
85+
)
86+
}

0 commit comments

Comments
 (0)