Skip to content

Commit e6a8594

Browse files
authored
test(react-router): Add e2e tests for react router framework SPA mode (#16390)
Adding e2e tests for SPA mode for react router framework. It was confirmed that navigation and client pageload instrumentations work. closes getsentry/sentry-docs#13856
1 parent 6e61f82 commit e6a8594

25 files changed

+2995
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
# misc
15+
.DS_Store
16+
.env.local
17+
.env.development.local
18+
.env.test.local
19+
.env.production.local
20+
21+
npm-debug.log*
22+
yarn-debug.log*
23+
yarn-error.log*
24+
25+
/test-results/
26+
/playwright-report/
27+
/playwright/.cache/
28+
29+
!*.d.ts
30+
31+
# react router
32+
.react-router
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
@import 'tailwindcss';
2+
3+
@theme {
4+
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
5+
'Noto Color Emoji';
6+
}
7+
8+
html,
9+
body {
10+
@apply bg-white dark:bg-gray-950;
11+
12+
@media (prefers-color-scheme: dark) {
13+
color-scheme: dark;
14+
}
15+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as Sentry from '@sentry/react-router';
2+
import { StrictMode, startTransition } from 'react';
3+
import { hydrateRoot } from 'react-dom/client';
4+
import { HydratedRouter } from 'react-router/dom';
5+
6+
Sentry.init({
7+
environment: 'qa', // dynamic sampling bias to keep transactions
8+
// todo: get this from env
9+
dsn: 'https://username@domain/123',
10+
integrations: [Sentry.reactRouterTracingIntegration()],
11+
tracesSampleRate: 1.0,
12+
tunnel: `http://localhost:3031/`, // proxy server
13+
tracePropagationTargets: [/^\//],
14+
});
15+
16+
startTransition(() => {
17+
hydrateRoot(
18+
document,
19+
<StrictMode>
20+
<HydratedRouter />
21+
</StrictMode>,
22+
);
23+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
isRouteErrorResponse,
3+
Links,
4+
Meta,
5+
Outlet,
6+
Scripts,
7+
ScrollRestoration,
8+
} from "react-router";
9+
10+
import type { Route } from "./+types/root";
11+
import "./app.css";
12+
import * as Sentry from '@sentry/react-router';
13+
14+
export function Layout({ children }: { children: React.ReactNode }) {
15+
return (
16+
<html lang="en">
17+
<head>
18+
<meta charSet="utf-8" />
19+
<meta name="viewport" content="width=device-width, initial-scale=1" />
20+
<Meta />
21+
<Links />
22+
</head>
23+
<body>
24+
{children}
25+
<ScrollRestoration />
26+
<Scripts />
27+
</body>
28+
</html>
29+
);
30+
}
31+
32+
export default function App() {
33+
return <Outlet />;
34+
}
35+
36+
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
37+
let message = "Oops!";
38+
let details = "An unexpected error occurred.";
39+
let stack: string | undefined;
40+
41+
if (isRouteErrorResponse(error)) {
42+
message = error.status === 404 ? "404" : "Error";
43+
details =
44+
error.status === 404
45+
? "The requested page could not be found."
46+
: error.statusText || details;
47+
} else if (error && error instanceof Error) {
48+
Sentry.captureException(error);
49+
50+
details = error.message;
51+
stack = error.stack;
52+
}
53+
54+
return (
55+
<main className="pt-16 p-4 container mx-auto">
56+
<h1>{message}</h1>
57+
<p>{details}</p>
58+
{stack && (
59+
<pre className="w-full p-4 overflow-x-auto">
60+
<code>{stack}</code>
61+
</pre>
62+
)}
63+
</main>
64+
);
65+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { type RouteConfig, index, prefix, route } from '@react-router/dev/routes';
2+
3+
export default [
4+
index('routes/home.tsx'),
5+
...prefix('errors', [
6+
route('client', 'routes/errors/client.tsx'),
7+
route('client/:client-param', 'routes/errors/client-param.tsx'),
8+
route('client-loader', 'routes/errors/client-loader.tsx'),
9+
route('client-action', 'routes/errors/client-action.tsx'),
10+
]),
11+
...prefix('performance', [
12+
index('routes/performance/index.tsx'),
13+
route('with/:param', 'routes/performance/dynamic-param.tsx'),
14+
]),
15+
] satisfies RouteConfig;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Form } from 'react-router';
2+
3+
export function clientAction() {
4+
throw new Error('Madonna mia! Che casino nella Client Action!');
5+
}
6+
7+
export default function ClientActionErrorPage() {
8+
return (
9+
<div>
10+
<h1>Client Error Action Page</h1>
11+
<Form method="post">
12+
<button id="submit" type="submit">
13+
Submit
14+
</button>
15+
</Form>
16+
</div>
17+
);
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { Route } from './+types/client-loader';
2+
3+
export function clientLoader() {
4+
throw new Error('¡Madre mía del client loader!');
5+
}
6+
7+
export default function ClientLoaderErrorPage({ loaderData }: Route.ComponentProps) {
8+
const { data } = loaderData ?? { data: 'sad' };
9+
return (
10+
<div>
11+
<h1>Client Loader Error Page</h1>
12+
<div>{data}</div>
13+
</div>
14+
);
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { Route } from './+types/client-param';
2+
3+
export default function ClientErrorParamPage({ params }: Route.ComponentProps) {
4+
return (
5+
<div>
6+
<h1>Client Error Param Page</h1>
7+
<button
8+
id="throw-on-click"
9+
onClick={() => {
10+
throw new Error(`¡Madre mía de ${params['client-param']}!`);
11+
}}
12+
>
13+
Throw Error
14+
</button>
15+
</div>
16+
);
17+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default function ClientErrorPage() {
2+
return (
3+
<div>
4+
<h1>Client Error Page</h1>
5+
<button
6+
id="throw-on-click"
7+
onClick={() => {
8+
throw new Error('¡Madre mía!');
9+
}}
10+
>
11+
Throw Error
12+
</button>
13+
</div>
14+
);
15+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { Route } from './+types/home';
2+
import { Link } from 'react-router';
3+
4+
export function meta({}: Route.MetaArgs) {
5+
return [{ title: 'New React Router App' }, { name: 'description', content: 'Welcome to React Router!' }];
6+
}
7+
8+
export default function Home() {
9+
return (
10+
<div>
11+
<h1>Hello,This is an SPA React Router app</h1>
12+
<div>
13+
<br />
14+
<br />
15+
<h2>Performance Pages, click pages to get redirected</h2>
16+
<ul>
17+
<li><Link to="/performance">Performance page</Link></li>
18+
<li><Link to="/performance/static">Static Page</Link></li>
19+
<li><Link to="/performance/dynamic-param/123">Dynamic Parameter Page</Link></li>
20+
</ul>
21+
</div>
22+
<div>
23+
<br />
24+
<h2>Error Pages, click button to trigger error</h2>
25+
<ul>
26+
<li><Link to="/errors/client">Client Error</Link></li>
27+
<li><Link to="/errors/client-action">Client Action Error</Link></li>
28+
<li><Link to="/errors/client-loader">Client Loader Error</Link></li>
29+
<li><Link to="/errors/client-param/123">Client Parameter Error</Link></li>
30+
</ul>
31+
</div>
32+
</div>
33+
);
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { Route } from './+types/dynamic-param';
2+
3+
4+
export default function DynamicParamPage({ params }: Route.ComponentProps) {
5+
const { param } = params;
6+
7+
return (
8+
<div>
9+
<h1>Dynamic Parameter Page</h1>
10+
<p>The parameter value is: {param}</p>
11+
</div>
12+
);
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Link } from 'react-router';
2+
3+
export default function PerformancePage() {
4+
return (
5+
<div>
6+
<h1>Performance Page</h1>
7+
<nav>
8+
<Link to="/performance/with/sentry">With Param Page</Link>
9+
</nav>
10+
</div>
11+
);
12+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"name": "react-router-7-framework-spa",
3+
"private": true,
4+
"version": "0.1.0",
5+
"type": "module",
6+
"scripts": {
7+
"build": "react-router build",
8+
"dev": "react-router dev",
9+
"start": "vite preview",
10+
"preview": "vite preview",
11+
"typecheck": "react-router typegen && tsc",
12+
"clean": "pnpx rimraf node_modules pnpm-lock.yaml",
13+
"test:build": "pnpm install && pnpm build",
14+
"test:ts": "pnpm typecheck",
15+
"test:prod": "playwright test",
16+
"test:dev": "TEST_ENV=development playwright test",
17+
"test:assert": "pnpm test:ts &&pnpm test:prod"
18+
},
19+
"dependencies": {
20+
"@sentry/react-router": "latest || *",
21+
"@react-router/node": "^7.5.3",
22+
"@react-router/serve": "^7.5.3",
23+
"isbot": "^5.1.27",
24+
"react": "^18.3.1",
25+
"react-dom": "^18.3.1",
26+
"react-router": "^7.1.5"
27+
},
28+
"devDependencies": {
29+
"@playwright/test": "^1.52.0",
30+
"@react-router/dev": "^7.5.3",
31+
"@sentry-internal/test-utils": "link:../../../test-utils",
32+
"@tailwindcss/vite": "^4.1.4",
33+
"@types/node": "^20",
34+
"@types/react": "^19.1.2",
35+
"@types/react-dom": "^19.1.2",
36+
"tailwindcss": "^4.1.4",
37+
"typescript": "^5.8.3",
38+
"vite": "^6.3.3",
39+
"vite-tsconfig-paths": "^5.1.4"
40+
},
41+
"browserslist": {
42+
"production": [
43+
">0.2%",
44+
"not dead",
45+
"not op_mini all"
46+
],
47+
"development": [
48+
"last 1 chrome version",
49+
"last 1 firefox version",
50+
"last 1 safari version"
51+
]
52+
},
53+
"volta": {
54+
"extends": "../../package.json"
55+
}
56+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig({
4+
startCommand: 'pnpm start',
5+
port: 4173,
6+
});
7+
8+
export default config;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { Config } from "@react-router/dev/config";
2+
3+
export default {
4+
ssr: false,
5+
} satisfies Config;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { startEventProxyServer } from '@sentry-internal/test-utils';
2+
3+
startEventProxyServer({
4+
port: 3031,
5+
proxyServerName: 'react-router-7-framework-spa',
6+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const APP_NAME = 'react-router-7-framework-spa';

0 commit comments

Comments
 (0)