Skip to content

Commit 751f0ff

Browse files
authored
test(vue-start): selective-ssr suite (#6124)
1 parent 3c77213 commit 751f0ff

23 files changed

+1215
-112
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
node_modules
2+
package-lock.json
3+
yarn.lock
4+
5+
.DS_Store
6+
.cache
7+
.env
8+
.vercel
9+
.output
10+
/build/
11+
/api/
12+
/server/build
13+
/public/build# Sentry Config File
14+
.env.sentry-build-plugin
15+
/test-results/
16+
/playwright-report/
17+
/blob-report/
18+
/playwright/.cache/
19+
20+
count.txt
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
**/build
2+
**/public
3+
pnpm-lock.yaml
4+
routeTree.gen.ts
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "tanstack-vue-start-e2e-selective-ssr",
3+
"private": true,
4+
"sideEffects": false,
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite dev --port 3000",
8+
"dev:e2e": "vite dev",
9+
"build": "vite build && tsc --noEmit",
10+
"preview": "vite preview",
11+
"start": "pnpx srvx --prod -s ../client dist/server/server.js",
12+
"test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
13+
},
14+
"dependencies": {
15+
"@tanstack/vue-router": "workspace:^",
16+
"@tanstack/vue-start": "workspace:^",
17+
"vue": "^3.5.25",
18+
"zod": "^3.24.2"
19+
},
20+
"devDependencies": {
21+
"@tailwindcss/postcss": "^4.1.15",
22+
"@tanstack/router-e2e-utils": "workspace:^",
23+
"postcss": "^8.5.1",
24+
"srvx": "^0.8.6",
25+
"tailwindcss": "^4.1.17",
26+
"typescript": "^5.7.2",
27+
"vite": "^7.1.7",
28+
"@vitejs/plugin-vue": "^6.0.3",
29+
"@vitejs/plugin-vue-jsx": "^5.1.2",
30+
"vite-tsconfig-paths": "^5.1.4",
31+
"vue-tsc": "^3.1.8"
32+
}
33+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { defineConfig, devices } from '@playwright/test'
2+
import { getTestServerPort } from '@tanstack/router-e2e-utils'
3+
import packageJson from './package.json' with { type: 'json' }
4+
5+
const PORT = await getTestServerPort(packageJson.name)
6+
const baseURL = `http://localhost:${PORT}`
7+
/**
8+
* See https://playwright.dev/docs/test-configuration.
9+
*/
10+
export default defineConfig({
11+
testDir: './tests',
12+
workers: 1,
13+
14+
reporter: [['line']],
15+
16+
use: {
17+
/* Base URL to use in actions like `await page.goto('/')`. */
18+
baseURL,
19+
},
20+
21+
webServer: {
22+
command: `VITE_SERVER_PORT=${PORT} pnpm build && NODE_ENV=production PORT=${PORT} VITE_SERVER_PORT=${PORT} pnpm start`,
23+
url: baseURL,
24+
reuseExistingServer: !process.env.CI,
25+
stdout: 'pipe',
26+
},
27+
28+
projects: [
29+
{
30+
name: 'chromium',
31+
use: { ...devices['Desktop Chrome'] },
32+
},
33+
],
34+
})
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default {
2+
plugins: {
3+
'@tailwindcss/postcss': {},
4+
},
5+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/* eslint-disable */
2+
3+
// @ts-nocheck
4+
5+
// noinspection JSUnusedGlobalSymbols
6+
7+
// This file was automatically generated by TanStack Router.
8+
// You should NOT make any changes in this file as it will be overwritten.
9+
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10+
11+
import { Route as rootRouteImport } from './routes/__root'
12+
import { Route as PostsRouteImport } from './routes/posts'
13+
import { Route as IndexRouteImport } from './routes/index'
14+
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
15+
16+
const PostsRoute = PostsRouteImport.update({
17+
id: '/posts',
18+
path: '/posts',
19+
getParentRoute: () => rootRouteImport,
20+
} as any)
21+
const IndexRoute = IndexRouteImport.update({
22+
id: '/',
23+
path: '/',
24+
getParentRoute: () => rootRouteImport,
25+
} as any)
26+
const PostsPostIdRoute = PostsPostIdRouteImport.update({
27+
id: '/$postId',
28+
path: '/$postId',
29+
getParentRoute: () => PostsRoute,
30+
} as any)
31+
32+
export interface FileRoutesByFullPath {
33+
'/': typeof IndexRoute
34+
'/posts': typeof PostsRouteWithChildren
35+
'/posts/$postId': typeof PostsPostIdRoute
36+
}
37+
export interface FileRoutesByTo {
38+
'/': typeof IndexRoute
39+
'/posts': typeof PostsRouteWithChildren
40+
'/posts/$postId': typeof PostsPostIdRoute
41+
}
42+
export interface FileRoutesById {
43+
__root__: typeof rootRouteImport
44+
'/': typeof IndexRoute
45+
'/posts': typeof PostsRouteWithChildren
46+
'/posts/$postId': typeof PostsPostIdRoute
47+
}
48+
export interface FileRouteTypes {
49+
fileRoutesByFullPath: FileRoutesByFullPath
50+
fullPaths: '/' | '/posts' | '/posts/$postId'
51+
fileRoutesByTo: FileRoutesByTo
52+
to: '/' | '/posts' | '/posts/$postId'
53+
id: '__root__' | '/' | '/posts' | '/posts/$postId'
54+
fileRoutesById: FileRoutesById
55+
}
56+
export interface RootRouteChildren {
57+
IndexRoute: typeof IndexRoute
58+
PostsRoute: typeof PostsRouteWithChildren
59+
}
60+
61+
declare module '@tanstack/vue-router' {
62+
interface FileRoutesByPath {
63+
'/posts': {
64+
id: '/posts'
65+
path: '/posts'
66+
fullPath: '/posts'
67+
preLoaderRoute: typeof PostsRouteImport
68+
parentRoute: typeof rootRouteImport
69+
}
70+
'/': {
71+
id: '/'
72+
path: '/'
73+
fullPath: '/'
74+
preLoaderRoute: typeof IndexRouteImport
75+
parentRoute: typeof rootRouteImport
76+
}
77+
'/posts/$postId': {
78+
id: '/posts/$postId'
79+
path: '/$postId'
80+
fullPath: '/posts/$postId'
81+
preLoaderRoute: typeof PostsPostIdRouteImport
82+
parentRoute: typeof PostsRoute
83+
}
84+
}
85+
}
86+
87+
interface PostsRouteChildren {
88+
PostsPostIdRoute: typeof PostsPostIdRoute
89+
}
90+
91+
const PostsRouteChildren: PostsRouteChildren = {
92+
PostsPostIdRoute: PostsPostIdRoute,
93+
}
94+
95+
const PostsRouteWithChildren = PostsRoute._addFileChildren(PostsRouteChildren)
96+
97+
const rootRouteChildren: RootRouteChildren = {
98+
IndexRoute: IndexRoute,
99+
PostsRoute: PostsRouteWithChildren,
100+
}
101+
export const routeTree = rootRouteImport
102+
._addFileChildren(rootRouteChildren)
103+
._addFileTypes<FileRouteTypes>()
104+
105+
import type { getRouter } from './router.tsx'
106+
import type { createStart } from '@tanstack/vue-start'
107+
declare module '@tanstack/vue-start' {
108+
interface Register {
109+
ssr: true
110+
router: Awaited<ReturnType<typeof getRouter>>
111+
}
112+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { createRouter } from '@tanstack/vue-router'
2+
import { routeTree } from './routeTree.gen'
3+
4+
export function getRouter() {
5+
const router = createRouter({
6+
routeTree,
7+
scrollRestoration: true,
8+
})
9+
10+
return router
11+
}
12+
13+
declare module '@tanstack/vue-router' {
14+
interface Register {
15+
router: ReturnType<typeof getRouter>
16+
}
17+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/// <reference types="vite/client" />
2+
import {
3+
Body,
4+
ClientOnly,
5+
HeadContent,
6+
Html,
7+
Link,
8+
Outlet,
9+
Scripts,
10+
createRootRoute,
11+
useRouterState,
12+
} from '@tanstack/vue-router'
13+
import { z } from 'zod'
14+
import { ssrSchema } from '~/search'
15+
import appCss from '~/styles/app.css?url'
16+
17+
export const Route = createRootRoute({
18+
head: () => ({
19+
meta: [
20+
{
21+
charSet: 'utf-8',
22+
},
23+
{
24+
name: 'viewport',
25+
content: 'width=device-width, initial-scale=1',
26+
},
27+
{
28+
title: 'Selective SSR E2E Test',
29+
},
30+
],
31+
links: [{ rel: 'stylesheet', href: appCss }],
32+
}),
33+
validateSearch: z.object({ root: ssrSchema }),
34+
ssr: ({ search }) => {
35+
if (typeof window !== 'undefined') {
36+
const error = `ssr() for ${Route.id} should not be called on the client`
37+
console.error(error)
38+
throw new Error(error)
39+
}
40+
if (search.status === 'success') {
41+
return search.value.root?.ssr
42+
}
43+
},
44+
beforeLoad: ({ search }) => {
45+
console.log(
46+
`beforeLoad for ${Route.id} called on the ${typeof window !== 'undefined' ? 'client' : 'server'}`,
47+
)
48+
if (
49+
search.root?.expected?.data === 'client' &&
50+
typeof window === 'undefined'
51+
) {
52+
const error = `Expected beforeLoad for ${Route.id} to be executed on the client, but it is running on the server`
53+
console.error(error)
54+
throw new Error(error)
55+
}
56+
return {
57+
root: typeof window === 'undefined' ? 'server' : 'client',
58+
search,
59+
}
60+
},
61+
loader: ({ context }) => {
62+
console.log(
63+
`loader for ${Route.id} called on the ${typeof window !== 'undefined' ? 'client' : 'server'}`,
64+
)
65+
66+
if (
67+
context.search.root?.expected?.data === 'client' &&
68+
typeof window === 'undefined'
69+
) {
70+
const error = `Expected loader for ${Route.id} to be executed on the client, but it is running on the server`
71+
console.error(error)
72+
throw new Error(error)
73+
}
74+
return { root: typeof window === 'undefined' ? 'server' : 'client' }
75+
},
76+
shellComponent: RootDocument,
77+
component: () => {
78+
const search = Route.useSearch()
79+
if (
80+
typeof window === 'undefined' &&
81+
search.value.root?.expected?.render === 'client-only'
82+
) {
83+
const error = `Expected component for ${Route.id} to be executed on the client, but it is running on the server`
84+
console.error(error)
85+
throw new Error(error)
86+
}
87+
const loaderData = Route.useLoaderData()
88+
const context = Route.useRouteContext()
89+
return (
90+
<div data-testid="root-container">
91+
<h2 data-testid="root-heading">root</h2>
92+
<div>
93+
ssr: <b>{JSON.stringify(search.value.root?.ssr ?? 'undefined')}</b>
94+
</div>
95+
<div>
96+
expected data location execution:{' '}
97+
<b data-testid="root-data-expected">
98+
{search.value.root?.expected?.data}
99+
</b>
100+
</div>
101+
<div>
102+
loader: <b data-testid="root-loader">{loaderData.value.root}</b>
103+
</div>
104+
<div>
105+
context: <b data-testid="root-context">{context.value.root}</b>
106+
</div>
107+
<hr />
108+
<Outlet />
109+
</div>
110+
)
111+
},
112+
})
113+
114+
function RootDocument(_: unknown, { slots }: { slots: any }) {
115+
const routerState = useRouterState({
116+
select: (state) => ({
117+
isLoading: state.isLoading,
118+
status: state.status,
119+
}),
120+
})
121+
return (
122+
<Html>
123+
<head>
124+
<HeadContent />
125+
</head>
126+
<Body>
127+
<div class="p-2 flex gap-2 text-lg">
128+
<h1>Selective SSR E2E Test</h1>
129+
<Link
130+
to="/"
131+
activeProps={{
132+
class: 'font-bold',
133+
}}
134+
>
135+
Home
136+
</Link>
137+
</div>
138+
<hr />
139+
<ClientOnly>
140+
<div>
141+
router isLoading:{' '}
142+
<b data-testid="router-isLoading">
143+
{routerState.value.isLoading ? 'true' : 'false'}
144+
</b>
145+
</div>
146+
<div>
147+
router status:{' '}
148+
<b data-testid="router-status">{routerState.value.status}</b>
149+
</div>
150+
</ClientOnly>
151+
<hr />
152+
{slots.default?.()}
153+
<Scripts />
154+
</Body>
155+
</Html>
156+
)
157+
}

0 commit comments

Comments
 (0)