-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
test(solid-router): js-only-file-based e2e suite #5626
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| node_modules | ||
| .DS_Store | ||
| dist | ||
| dist-hash | ||
| dist-ssr | ||
| *.local | ||
|
|
||
| /test-results/ | ||
| /playwright-report/ | ||
| /blob-report/ | ||
| /playwright/.cache/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Vite App</title> | ||
| </head> | ||
| <body> | ||
| <div id="app"></div> | ||
| <script type="module" src="/src/main.jsx"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| { | ||
| "name": "tanstack-router-e2e-solid-js-only-file-based", | ||
| "private": true, | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "vite --port 3000", | ||
| "dev:e2e": "vite", | ||
| "build": "vite build && tsc --noEmit", | ||
| "serve": "vite preview", | ||
| "start": "vite", | ||
| "test:e2e": "rm -rf port*.txt; playwright test --project=chromium" | ||
| }, | ||
| "dependencies": { | ||
| "@tailwindcss/postcss": "^4.1.15", | ||
| "@tanstack/solid-router": "workspace:^", | ||
| "@tanstack/solid-router-devtools": "workspace:^", | ||
| "postcss": "^8.5.1", | ||
| "solid-js": "^1.9.9", | ||
| "redaxios": "^0.5.1", | ||
| "tailwindcss": "^4.1.15" | ||
| }, | ||
| "devDependencies": { | ||
| "@playwright/test": "^1.50.1", | ||
| "@tanstack/router-e2e-utils": "workspace:^", | ||
| "@tanstack/router-plugin": "workspace:^", | ||
| "vite-plugin-solid": "^2.11.10", | ||
| "vite": "^7.1.7" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { defineConfig, devices } from '@playwright/test' | ||
| import { | ||
| getDummyServerPort, | ||
| getTestServerPort, | ||
| } from '@tanstack/router-e2e-utils' | ||
| import packageJson from './package.json' with { type: 'json' } | ||
|
|
||
| const PORT = await getTestServerPort(packageJson.name) | ||
| const EXTERNAL_PORT = await getDummyServerPort(packageJson.name) | ||
| const baseURL = `http://localhost:${PORT}` | ||
|
|
||
| /** | ||
| * See https://playwright.dev/docs/test-configuration. | ||
| */ | ||
| export default defineConfig({ | ||
| testDir: './tests', | ||
| workers: 1, | ||
|
|
||
| reporter: [['line']], | ||
|
|
||
| globalSetup: './tests/setup/global.setup.ts', | ||
| globalTeardown: './tests/setup/global.teardown.ts', | ||
|
|
||
| use: { | ||
| /* Base URL to use in actions like `await page.goto('/')`. */ | ||
| baseURL, | ||
| }, | ||
|
|
||
| webServer: { | ||
| command: `VITE_NODE_ENV="test" VITE_EXTERNAL_PORT=${EXTERNAL_PORT} VITE_SERVER_PORT=${PORT} pnpm build && pnpm serve --port ${PORT}`, | ||
| url: baseURL, | ||
| reuseExistingServer: !process.env.CI, | ||
| stdout: 'pipe', | ||
| }, | ||
|
|
||
| projects: [ | ||
| { | ||
| name: 'chromium', | ||
| use: { ...devices['Desktop Chrome'] }, | ||
| }, | ||
| ], | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export default { | ||
| plugins: { | ||
| '@tailwindcss/postcss': {}, | ||
| }, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { RouterProvider, createRouter } from '@tanstack/solid-router' | ||
| import { routeTree } from './routeTree.gen' | ||
| import { render } from 'solid-js/web' | ||
| import './styles.css' | ||
|
|
||
| // Set up a Router instance | ||
| const router = createRouter({ | ||
| routeTree, | ||
| defaultPreload: 'intent', | ||
| defaultStaleTime: 5000, | ||
| scrollRestoration: true, | ||
| }) | ||
|
|
||
| const rootElement = document.getElementById('app') | ||
|
|
||
| if (!rootElement.innerHTML) { | ||
| render(() => <RouterProvider router={router} />, rootElement) | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,27 @@ | ||||||||||||||
| import axios from 'redaxios' | ||||||||||||||
|
|
||||||||||||||
| export class NotFoundError extends Error {} | ||||||||||||||
|
|
||||||||||||||
| let queryURL = 'https://jsonplaceholder.typicode.com' | ||||||||||||||
|
|
||||||||||||||
| if (import.meta.env.VITE_NODE_ENV === 'test') { | ||||||||||||||
| queryURL = `http://localhost:${import.meta.env.VITE_EXTERNAL_PORT}` | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| export const fetchPosts = async () => { | ||||||||||||||
| console.info('Fetching posts...') | ||||||||||||||
| return axios.get(`${queryURL}/posts`).then((r) => r.data.slice(0, 10)) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| export const fetchPost = async (postId) => { | ||||||||||||||
| console.info(`Fetching post with id ${postId}...`) | ||||||||||||||
| const post = await axios | ||||||||||||||
| .get(`${queryURL}/posts/${postId}`) | ||||||||||||||
| .then((r) => r.data) | ||||||||||||||
|
|
||||||||||||||
| if (!post) { | ||||||||||||||
| throw new NotFoundError(`Post with id "${postId}" not found!`) | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+22
to
+24
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Strengthen the post validation logic. The check Apply this diff to add more robust validation: - if (!post) {
+ if (!post || !post.id) {
throw new NotFoundError(`Post with id "${postId}" not found!`)
}Or use more comprehensive validation: - if (!post) {
+ if (!post || typeof post !== 'object' || !post.id || !post.title) {
throw new NotFoundError(`Post with id "${postId}" not found!`)
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| return post | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| /* eslint-disable */ | ||
|
|
||
| // @ts-nocheck | ||
|
|
||
| // noinspection JSUnusedGlobalSymbols | ||
|
|
||
| // This file was automatically generated by TanStack Router. | ||
| // You should NOT make any changes in this file as it will be overwritten. | ||
| // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. | ||
|
|
||
| import { Route as rootRouteImport } from './routes/__root' | ||
| import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout' | ||
| import { Route as PostsRouteRouteImport } from './routes/posts.route' | ||
| import { Route as IndexRouteImport } from './routes/index' | ||
| import { Route as PostsIndexRouteImport } from './routes/posts.index' | ||
| import { Route as PostsPostIdRouteImport } from './routes/posts.$postId' | ||
| import { Route as PathlessLayoutNestedLayoutRouteImport } from './routes/_pathlessLayout/_nested-layout' | ||
| import { Route as PathlessLayoutNestedLayoutRouteBRouteImport } from './routes/_pathlessLayout/_nested-layout/route-b' | ||
| import { Route as PathlessLayoutNestedLayoutRouteARouteImport } from './routes/_pathlessLayout/_nested-layout/route-a' | ||
|
|
||
| const PathlessLayoutRoute = PathlessLayoutRouteImport.update({ | ||
| id: '/_pathlessLayout', | ||
| getParentRoute: () => rootRouteImport, | ||
| }) | ||
| const PostsRouteRoute = PostsRouteRouteImport.update({ | ||
| id: '/posts', | ||
| path: '/posts', | ||
| getParentRoute: () => rootRouteImport, | ||
| }) | ||
| const IndexRoute = IndexRouteImport.update({ | ||
| id: '/', | ||
| path: '/', | ||
| getParentRoute: () => rootRouteImport, | ||
| }) | ||
| const PostsIndexRoute = PostsIndexRouteImport.update({ | ||
| id: '/', | ||
| path: '/', | ||
| getParentRoute: () => PostsRouteRoute, | ||
| }) | ||
| const PostsPostIdRoute = PostsPostIdRouteImport.update({ | ||
| id: '/$postId', | ||
| path: '/$postId', | ||
| getParentRoute: () => PostsRouteRoute, | ||
| }) | ||
| const PathlessLayoutNestedLayoutRoute = | ||
| PathlessLayoutNestedLayoutRouteImport.update({ | ||
| id: '/_nested-layout', | ||
| getParentRoute: () => PathlessLayoutRoute, | ||
| }) | ||
| const PathlessLayoutNestedLayoutRouteBRoute = | ||
| PathlessLayoutNestedLayoutRouteBRouteImport.update({ | ||
| id: '/route-b', | ||
| path: '/route-b', | ||
| getParentRoute: () => PathlessLayoutNestedLayoutRoute, | ||
| }) | ||
| const PathlessLayoutNestedLayoutRouteARoute = | ||
| PathlessLayoutNestedLayoutRouteARouteImport.update({ | ||
| id: '/route-a', | ||
| path: '/route-a', | ||
| getParentRoute: () => PathlessLayoutNestedLayoutRoute, | ||
| }) | ||
|
|
||
| const PostsRouteRouteChildren = { | ||
| PostsPostIdRoute: PostsPostIdRoute, | ||
| PostsIndexRoute: PostsIndexRoute, | ||
| } | ||
|
|
||
| const PostsRouteRouteWithChildren = PostsRouteRoute._addFileChildren( | ||
| PostsRouteRouteChildren, | ||
| ) | ||
|
|
||
| const PathlessLayoutNestedLayoutRouteChildren = { | ||
| PathlessLayoutNestedLayoutRouteARoute: PathlessLayoutNestedLayoutRouteARoute, | ||
| PathlessLayoutNestedLayoutRouteBRoute: PathlessLayoutNestedLayoutRouteBRoute, | ||
| } | ||
|
|
||
| const PathlessLayoutNestedLayoutRouteWithChildren = | ||
| PathlessLayoutNestedLayoutRoute._addFileChildren( | ||
| PathlessLayoutNestedLayoutRouteChildren, | ||
| ) | ||
|
|
||
| const PathlessLayoutRouteChildren = { | ||
| PathlessLayoutNestedLayoutRoute: PathlessLayoutNestedLayoutRouteWithChildren, | ||
| } | ||
|
|
||
| const PathlessLayoutRouteWithChildren = PathlessLayoutRoute._addFileChildren( | ||
| PathlessLayoutRouteChildren, | ||
| ) | ||
|
|
||
| const rootRouteChildren = { | ||
| IndexRoute: IndexRoute, | ||
| PostsRouteRoute: PostsRouteRouteWithChildren, | ||
| PathlessLayoutRoute: PathlessLayoutRouteWithChildren, | ||
| } | ||
| export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import { Link, Outlet, createRootRoute } from '@tanstack/solid-router' | ||
| import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools' | ||
|
|
||
| export const Route = createRootRoute({ | ||
| component: RootComponent, | ||
| notFoundComponent: () => { | ||
| return ( | ||
| <div> | ||
| <p>This is the notFoundComponent configured on root route</p> | ||
| <Link to="/">Start Over</Link> | ||
| </div> | ||
| ) | ||
| }, | ||
| }) | ||
|
|
||
| function RootComponent() { | ||
| return ( | ||
| <> | ||
| <div class="p-2 flex gap-2 text-lg border-b"> | ||
| <Link | ||
| to="/" | ||
| activeProps={{ | ||
| class: 'font-bold', | ||
| }} | ||
| activeOptions={{ exact: true }} | ||
| > | ||
| Home | ||
| </Link>{' '} | ||
| <Link | ||
| to="/posts" | ||
| activeProps={{ | ||
| class: 'font-bold', | ||
| }} | ||
| > | ||
| Posts | ||
| </Link>{' '} | ||
| <Link | ||
| to="/route-a" | ||
| activeProps={{ | ||
| class: 'font-bold', | ||
| }} | ||
| > | ||
| Pathless Layout | ||
| </Link>{' '} | ||
| <Link | ||
| // @ts-expect-error | ||
| to="/this-route-does-not-exist" | ||
| activeProps={{ | ||
| class: 'font-bold', | ||
| }} | ||
| > | ||
| This Route Does Not Exist | ||
| </Link> | ||
| </div> | ||
| <hr /> | ||
| <Outlet /> | ||
| {/* Start rendering router matches */} | ||
| <TanStackRouterDevtools position="bottom-right" /> | ||
| </> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { createFileRoute } from '@tanstack/solid-router' | ||
| import { Outlet } from '@tanstack/solid-router' | ||
|
|
||
| export const Route = createFileRoute('/_pathlessLayout')({ | ||
| component: LayoutComponent, | ||
| }) | ||
|
|
||
| function LayoutComponent() { | ||
| return ( | ||
| <div class="p-2"> | ||
| <div class="border-b">I'm a pathless layout</div> | ||
| <div> | ||
| <Outlet /> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { createFileRoute } from '@tanstack/solid-router' | ||
| import { Link, Outlet } from '@tanstack/solid-router' | ||
|
|
||
| export const Route = createFileRoute('/_pathlessLayout/_nested-layout')({ | ||
| component: LayoutComponent, | ||
| }) | ||
|
|
||
| function LayoutComponent() { | ||
| return ( | ||
| <div> | ||
| <div>I'm a nested pathless layout</div> | ||
| <div class="flex gap-2 border-b"> | ||
| <Link | ||
| to="/route-a" | ||
| activeProps={{ | ||
| class: 'font-bold', | ||
| }} | ||
| > | ||
| Go to route A | ||
| </Link> | ||
| <Link | ||
| to="/route-b" | ||
| activeProps={{ | ||
| class: 'font-bold', | ||
| }} | ||
| > | ||
| Go to route B | ||
| </Link> | ||
| </div> | ||
| <div> | ||
| <Outlet /> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { createFileRoute } from '@tanstack/solid-router' | ||
| export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-a')( | ||
| { | ||
| component: LayoutAComponent, | ||
| }, | ||
| ) | ||
|
|
||
| function LayoutAComponent() { | ||
| return <div>I'm layout A!</div> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { createFileRoute } from '@tanstack/solid-router' | ||
| export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-b')( | ||
| { | ||
| component: LayoutBComponent, | ||
| }, | ||
| ) | ||
|
|
||
| function LayoutBComponent() { | ||
| return <div>I'm layout B!</div> | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add null safety check for rootElement.
Accessing
.innerHTMLon a potentially null element will throw a runtime error if the#appelement doesn't exist in the DOM.Apply this diff to add proper null safety:
const rootElement = document.getElementById('app') -if (!rootElement.innerHTML) { +if (rootElement && !rootElement.innerHTML) { render(() => <RouterProvider router={router} />, rootElement) }📝 Committable suggestion
🤖 Prompt for AI Agents