diff --git a/e2e/react-router/basic-react-query-file-based/src/routes/transition/count/query.tsx b/e2e/react-router/basic-react-query-file-based/src/routes/transition/count/query.tsx index ebd580af9ad..5bd2ff4341a 100644 --- a/e2e/react-router/basic-react-query-file-based/src/routes/transition/count/query.tsx +++ b/e2e/react-router/basic-react-query-file-based/src/routes/transition/count/query.tsx @@ -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' @@ -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, }) @@ -44,8 +44,10 @@ function TransitionPage() {
-
n: {search.n}
-
double: {doubleQuery.data}
+
n: {doubleQuery.data?.n}
+
+ double: {doubleQuery.data?.double} +
diff --git a/e2e/react-router/basic-react-query-file-based/tests/transition.spec.ts b/e2e/react-router/basic-react-query-file-based/tests/transition.spec.ts index 86110dcdc85..27ea079ca4e 100644 --- a/e2e/react-router/basic-react-query-file-based/tests/transition.spec.ts +++ b/e2e/react-router/basic-react-query-file-based/tests/transition.spec.ts @@ -1,6 +1,6 @@ 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') @@ -8,30 +8,88 @@ test('react-query transitions keep previous data during navigation', async ({ await expect(page.getByTestId('n-value')).toContainText('n: 1') await expect(page.getByTestId('double-value')).toContainText('double: 2') - const bodySnapshots: Array = [] + const bodyTexts: Array = [] - 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.', + ) + } }) diff --git a/e2e/react-start/basic-react-query/package.json b/e2e/react-start/basic-react-query/package.json index 5a5d538fba1..72881176eab 100644 --- a/e2e/react-start/basic-react-query/package.json +++ b/e2e/react-start/basic-react-query/package.json @@ -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", diff --git a/e2e/react-start/basic-react-query/src/routeTree.gen.ts b/e2e/react-start/basic-react-query/src/routeTree.gen.ts index c39b0ec44e0..c140825485d 100644 --- a/e2e/react-start/basic-react-query/src/routeTree.gen.ts +++ b/e2e/react-start/basic-react-query/src/routeTree.gen.ts @@ -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' @@ -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', @@ -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 @@ -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 @@ -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 @@ -181,6 +190,7 @@ export interface FileRouteTypes { | '/layout-b' | '/api/users/$id' | '/posts/$postId/deep' + | '/transition/count/query' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -196,6 +206,7 @@ export interface FileRouteTypes { | '/layout-b' | '/api/users/$id' | '/posts/$postId/deep' + | '/transition/count/query' id: | '__root__' | '/' @@ -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 { @@ -227,6 +239,7 @@ export interface RootRouteChildren { UsersRoute: typeof UsersRouteWithChildren ApiUsersRoute: typeof ApiUsersRouteWithChildren PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute + TransitionCountQueryRoute: typeof TransitionCountQueryRoute } declare module '@tanstack/react-router' { @@ -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' @@ -424,6 +444,7 @@ const rootRouteChildren: RootRouteChildren = { UsersRoute: UsersRouteWithChildren, ApiUsersRoute: ApiUsersRouteWithChildren, PostsPostIdDeepRoute: PostsPostIdDeepRoute, + TransitionCountQueryRoute: TransitionCountQueryRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/e2e/react-start/basic-react-query/src/routes/transition/count/query.tsx b/e2e/react-start/basic-react-query/src/routes/transition/count/query.tsx new file mode 100644 index 00000000000..5bd2ff4341a --- /dev/null +++ b/e2e/react-start/basic-react-query/src/routes/transition/count/query.tsx @@ -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 ( + +
+ ({ n: s.n + 1 })} + > + Increase + + +
+
n: {doubleQuery.data?.n}
+
+ double: {doubleQuery.data?.double} +
+
+
+
+ ) +} diff --git a/e2e/react-start/basic-react-query/tests/transition.spec.ts b/e2e/react-start/basic-react-query/tests/transition.spec.ts new file mode 100644 index 00000000000..2c28fc72a28 --- /dev/null +++ b/e2e/react-start/basic-react-query/tests/transition.spec.ts @@ -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 = [] + + 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() + + 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.', + ) + } +}) diff --git a/e2e/solid-router/basic-solid-query-file-based/src/routes/transition/count/query.tsx b/e2e/solid-router/basic-solid-query-file-based/src/routes/transition/count/query.tsx index 14964faad0d..256b097dd94 100644 --- a/e2e/solid-router/basic-solid-query-file-based/src/routes/transition/count/query.tsx +++ b/e2e/solid-router/basic-solid-query-file-based/src/routes/transition/count/query.tsx @@ -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, }) @@ -44,8 +44,10 @@ function TransitionPage() {
-
n: {search().n}
-
double: {doubleQuery.data}
+
n: {doubleQuery.data?.n}
+
+ double: {doubleQuery.data?.double} +
diff --git a/e2e/solid-router/basic-solid-query-file-based/tests/transition.spec.ts b/e2e/solid-router/basic-solid-query-file-based/tests/transition.spec.ts index 78a0e4341b7..27ea079ca4e 100644 --- a/e2e/solid-router/basic-solid-query-file-based/tests/transition.spec.ts +++ b/e2e/solid-router/basic-solid-query-file-based/tests/transition.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test' -test('solid-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') @@ -8,30 +8,88 @@ test('solid-query transitions keep previous data during navigation', async ({ await expect(page.getByTestId('n-value')).toContainText('n: 1') await expect(page.getByTestId('double-value')).toContainText('double: 2') - const bodySnapshots: Array = [] + const bodyTexts: Array = [] - 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.', + ) + } }) diff --git a/e2e/solid-start/basic-solid-query/package.json b/e2e/solid-start/basic-solid-query/package.json index fc6f36c0338..10b40e05a57 100644 --- a/e2e/solid-start/basic-solid-query/package.json +++ b/e2e/solid-start/basic-solid-query/package.json @@ -20,7 +20,8 @@ "redaxios": "^0.5.1", "solid-js": "^1.9.10", "tailwind-merge": "^2.6.0", - "vite": "^7.1.7" + "vite": "^7.1.7", + "zod": "^4.1.12" }, "devDependencies": { "@playwright/test": "^1.50.1", diff --git a/e2e/solid-start/basic-solid-query/src/routeTree.gen.ts b/e2e/solid-start/basic-solid-query/src/routeTree.gen.ts index 5a08920bd23..c100f876d5c 100644 --- a/e2e/solid-start/basic-solid-query/src/routeTree.gen.ts +++ b/e2e/solid-start/basic-solid-query/src/routeTree.gen.ts @@ -21,6 +21,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' @@ -84,6 +85,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', @@ -120,6 +126,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 @@ -134,6 +141,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 @@ -153,6 +161,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 @@ -171,6 +180,7 @@ export interface FileRouteTypes { | '/layout-b' | '/api/users/$id' | '/posts/$postId/deep' + | '/transition/count/query' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -185,6 +195,7 @@ export interface FileRouteTypes { | '/layout-b' | '/api/users/$id' | '/posts/$postId/deep' + | '/transition/count/query' id: | '__root__' | '/' @@ -203,6 +214,7 @@ export interface FileRouteTypes { | '/_layout/_layout-2/layout-b' | '/api/users/$id' | '/posts_/$postId/deep' + | '/transition/count/query' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -214,6 +226,7 @@ export interface RootRouteChildren { UsersRoute: typeof UsersRouteWithChildren ApiUsersRoute: typeof ApiUsersRouteWithChildren PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute + TransitionCountQueryRoute: typeof TransitionCountQueryRoute } declare module '@tanstack/solid-router' { @@ -302,6 +315,13 @@ declare module '@tanstack/solid-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' @@ -403,6 +423,7 @@ const rootRouteChildren: RootRouteChildren = { UsersRoute: UsersRouteWithChildren, ApiUsersRoute: ApiUsersRouteWithChildren, PostsPostIdDeepRoute: PostsPostIdDeepRoute, + TransitionCountQueryRoute: TransitionCountQueryRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/e2e/solid-start/basic-solid-query/src/routes/transition/count/query.tsx b/e2e/solid-start/basic-solid-query/src/routes/transition/count/query.tsx new file mode 100644 index 00000000000..256b097dd94 --- /dev/null +++ b/e2e/solid-start/basic-solid-query/src/routes/transition/count/query.tsx @@ -0,0 +1,55 @@ +import { Link, createFileRoute } from '@tanstack/solid-router' +import { Suspense } from 'solid-js' +import { queryOptions, useQuery } from '@tanstack/solid-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 ( + +
+ ({ n: s.n + 1 })} + > + Increase + + +
+
n: {doubleQuery.data?.n}
+
+ double: {doubleQuery.data?.double} +
+
+
+
+ ) +} diff --git a/e2e/solid-start/basic-solid-query/tests/transition.spec.ts b/e2e/solid-start/basic-solid-query/tests/transition.spec.ts new file mode 100644 index 00000000000..2c28fc72a28 --- /dev/null +++ b/e2e/solid-start/basic-solid-query/tests/transition.spec.ts @@ -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 = [] + + 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() + + 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.', + ) + } +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc8790f6853..81919ec9532 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1221,6 +1221,9 @@ importers: vite: specifier: ^7.1.7 version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + zod: + specifier: ^4.1.12 + version: 4.1.12 devDependencies: '@playwright/test': specifier: ^1.52.0 @@ -2829,6 +2832,9 @@ importers: vite: specifier: ^7.1.7 version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + zod: + specifier: ^4.1.12 + version: 4.1.12 devDependencies: '@playwright/test': specifier: ^1.52.0 @@ -5853,7 +5859,7 @@ importers: version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + version: 3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6(@types/node@22.10.2)(playwright@1.52.0)(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@3.2.4))(@vitest/ui@3.0.6(vitest@3.2.4))(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) web-vitals: specifier: ^5.1.0 version: 5.1.0 @@ -30878,7 +30884,7 @@ snapshots: optionalDependencies: vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) - vitest@3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1): + vitest@3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6(@types/node@22.10.2)(playwright@1.52.0)(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@3.2.4))(@vitest/ui@3.0.6(vitest@3.2.4))(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 @@ -30907,7 +30913,7 @@ snapshots: '@types/node': 22.10.2 '@vitest/browser': 3.0.6(@types/node@22.10.2)(playwright@1.52.0)(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@3.2.4) '@vitest/ui': 3.0.6(vitest@3.2.4) - jsdom: 25.0.1 + jsdom: 27.0.0(postcss@8.5.6) transitivePeerDependencies: - jiti - less @@ -30922,7 +30928,7 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1): + vitest@3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 @@ -30951,7 +30957,7 @@ snapshots: '@types/node': 22.10.2 '@vitest/browser': 3.0.6(@types/node@22.10.2)(playwright@1.52.0)(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@3.2.4) '@vitest/ui': 3.0.6(vitest@3.2.4) - jsdom: 27.0.0(postcss@8.5.6) + jsdom: 25.0.1 transitivePeerDependencies: - jiti - less