Skip to content

Commit 0bd48c8

Browse files
committed
Add an E2E test.
1 parent 673d5cf commit 0bd48c8

File tree

13 files changed

+354
-0
lines changed

13 files changed

+354
-0
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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
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: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"name": "react-router-6-descendant-routes",
3+
"version": "0.1.0",
4+
"private": true,
5+
"dependencies": {
6+
"@sentry/react": "latest || *",
7+
"@types/react": "18.0.0",
8+
"@types/react-dom": "18.0.0",
9+
"express": "4.19.2",
10+
"react": "18.2.0",
11+
"react-dom": "18.2.0",
12+
"react-router-dom": "^6.28.0",
13+
"react-scripts": "5.0.1",
14+
"typescript": "4.9.5"
15+
},
16+
"scripts": {
17+
"build": "react-scripts build",
18+
"start": "run-p start:client start:server",
19+
"start:client": "node server/app.js",
20+
"start:server": "serve -s build",
21+
"test": "playwright test",
22+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
23+
"test:build": "pnpm install && npx playwright install && pnpm build",
24+
"test:build-ts3.8": "pnpm install && pnpm add [email protected] && npx playwright install && pnpm build",
25+
"test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && npx playwright install && pnpm build",
26+
"test:assert": "pnpm test"
27+
},
28+
"eslintConfig": {
29+
"extends": [
30+
"react-app",
31+
"react-app/jest"
32+
]
33+
},
34+
"browserslist": {
35+
"production": [
36+
">0.2%",
37+
"not dead",
38+
"not op_mini all"
39+
],
40+
"development": [
41+
"last 1 chrome version",
42+
"last 1 firefox version",
43+
"last 1 safari version"
44+
]
45+
},
46+
"devDependencies": {
47+
"@playwright/test": "^1.44.1",
48+
"@sentry-internal/test-utils": "link:../../../test-utils",
49+
"serve": "14.0.1",
50+
"npm-run-all2": "^6.2.0"
51+
},
52+
"volta": {
53+
"extends": "../../package.json"
54+
},
55+
"sentryTest": {
56+
"variants": [
57+
{
58+
"build-command": "test:build-ts3.8",
59+
"label": "react-router-6 (TS 3.8)"
60+
}
61+
]
62+
}
63+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig({
4+
startCommand: `pnpm start`,
5+
});
6+
7+
export default config;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<meta name="theme-color" content="#000000" />
7+
<meta name="description" content="Web site created using create-react-app" />
8+
<title>React App</title>
9+
</head>
10+
<body>
11+
<noscript>You need to enable JavaScript to run this app.</noscript>
12+
<div id="root"></div>
13+
<!--
14+
This HTML file is a template.
15+
If you open it directly in the browser, you will see an empty page.
16+
17+
You can add webfonts, meta tags, or analytics to this file.
18+
The build step will place the bundled scripts into the <body> tag.
19+
20+
To begin the development, run `npm start` or `yarn start`.
21+
To create a production bundle, use `npm run build` or `yarn build`.
22+
-->
23+
</body>
24+
</html>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const express = require('express');
2+
3+
const app = express();
4+
const PORT = 8080;
5+
6+
const wait = time => {
7+
return new Promise(resolve => {
8+
setTimeout(() => {
9+
resolve();
10+
}, time);
11+
});
12+
};
13+
14+
async function sseHandler(request, response, timeout = false) {
15+
response.headers = {
16+
'Content-Type': 'text/event-stream',
17+
Connection: 'keep-alive',
18+
'Cache-Control': 'no-cache',
19+
'Access-Control-Allow-Origin': '*',
20+
};
21+
22+
response.setHeader('Cache-Control', 'no-cache');
23+
response.setHeader('Content-Type', 'text/event-stream');
24+
response.setHeader('Access-Control-Allow-Origin', '*');
25+
response.setHeader('Connection', 'keep-alive');
26+
27+
response.flushHeaders();
28+
29+
await wait(2000);
30+
31+
for (let index = 0; index < 10; index++) {
32+
response.write(`data: ${new Date().toISOString()}\n\n`);
33+
if (timeout) {
34+
await wait(10000);
35+
}
36+
}
37+
38+
response.end();
39+
}
40+
41+
app.get('/sse', (req, res) => sseHandler(req, res));
42+
43+
app.get('/sse-timeout', (req, res) => sseHandler(req, res, true));
44+
45+
app.listen(PORT, () => {
46+
console.log(`SSE service listening at http://localhost:${PORT}`);
47+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
interface Window {
2+
recordedTransactions?: string[];
3+
capturedExceptionId?: string;
4+
sentryReplayId?: string;
5+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import * as Sentry from '@sentry/react';
2+
import React from 'react';
3+
import ReactDOM from 'react-dom/client';
4+
import {
5+
BrowserRouter,
6+
Route,
7+
Routes,
8+
createRoutesFromChildren,
9+
matchRoutes,
10+
useLocation,
11+
useNavigationType,
12+
Navigate,
13+
Outlet,
14+
} from 'react-router-dom';
15+
16+
const replay = Sentry.replayIntegration();
17+
18+
Sentry.init({
19+
environment: 'qa', // dynamic sampling bias to keep transactions
20+
dsn: process.env.REACT_APP_E2E_TEST_DSN,
21+
integrations: [
22+
Sentry.reactRouterV6BrowserTracingIntegration({
23+
useEffect: React.useEffect,
24+
useLocation,
25+
useNavigationType,
26+
createRoutesFromChildren,
27+
matchRoutes,
28+
trackFetchStreamPerformance: true,
29+
}),
30+
replay,
31+
],
32+
// We recommend adjusting this value in production, or using tracesSampler
33+
// for finer control
34+
tracesSampleRate: 1.0,
35+
release: 'e2e-test',
36+
37+
// Always capture replays, so we can test this properly
38+
replaysSessionSampleRate: 1.0,
39+
replaysOnErrorSampleRate: 0.0,
40+
41+
tunnel: 'http://localhost:3031',
42+
});
43+
44+
const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
45+
46+
const ProjectsRoutes = () => (
47+
<SentryRoutes>
48+
<Route path=":projectId" element={<div>Project Page</div>}>
49+
<Route index element={<div>Project Page Root</div>} />
50+
<Route element={<div>Editor</div>}>
51+
<Route path="*" element={<Outlet />}>
52+
<Route path="views/:viewId" element={<div>View Canvas</div>} />
53+
</Route>
54+
</Route>
55+
</Route>
56+
<Route path="*" element={<div>No Match Page</div>} />
57+
</SentryRoutes>
58+
);
59+
60+
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
61+
root.render(
62+
<BrowserRouter>
63+
<SentryRoutes>
64+
{/* <Route index element={<Navigate to="/projects/123/views/234" />} /> */}
65+
<Route path="projects/*" element={<ProjectsRoutes />}></Route>
66+
</SentryRoutes>
67+
</BrowserRouter>,
68+
);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX
2+
import * as React from 'react';
3+
import { Link } from 'react-router-dom';
4+
5+
const Index = () => {
6+
return (
7+
<>
8+
<input
9+
type="button"
10+
value="Capture Exception"
11+
id="exception-button"
12+
onClick={() => {
13+
throw new Error('I am an error!');
14+
}}
15+
/>
16+
<Link to="/user/5" id="navigation">
17+
navigate
18+
</Link>
19+
</>
20+
);
21+
};
22+
23+
export default Index;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="react-scripts" />
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-6-descendant-routes',
6+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForTransaction } from '@sentry-internal/test-utils';
3+
4+
test('sends a pageload transaction with a parameterized URL', async ({ page }) => {
5+
const transactionPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => {
6+
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
7+
});
8+
9+
await page.goto(`/projects/123/views/234`);
10+
11+
const rootSpan = await transactionPromise;
12+
13+
console.debug('rootSpan', rootSpan);
14+
15+
expect(rootSpan).toMatchObject({
16+
contexts: {
17+
trace: {
18+
op: 'pageload',
19+
origin: 'auto.pageload.react.reactrouter_v6',
20+
},
21+
},
22+
transaction: '/projects/:projectId/views/:viewId',
23+
transaction_info: {
24+
source: 'route',
25+
},
26+
});
27+
});
28+
29+
// test('sends a navigation transaction with a parameterized URL', async ({ page }) => {
30+
// page.on('console', msg => console.log(msg.text()));
31+
32+
// const pageloadTxnPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => {
33+
// return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
34+
// });
35+
36+
// const navigationTxnPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => {
37+
// return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
38+
// });
39+
40+
// await page.goto(`/`);
41+
// await pageloadTxnPromise;
42+
43+
// const linkElement = page.locator('id=navigation');
44+
45+
// const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]);
46+
47+
// expect(navigationTxn).toMatchObject({
48+
// contexts: {
49+
// trace: {
50+
// op: 'navigation',
51+
// origin: 'auto.navigation.react.reactrouter_v6',
52+
// },
53+
// },
54+
// transaction: '/user/:id',
55+
// transaction_info: {
56+
// source: 'route',
57+
// },
58+
// });
59+
// });
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2018",
4+
"lib": ["dom", "dom.iterable", "esnext"],
5+
"allowJs": true,
6+
"skipLibCheck": true,
7+
"esModuleInterop": true,
8+
"allowSyntheticDefaultImports": true,
9+
"strict": true,
10+
"forceConsistentCasingInFileNames": true,
11+
"noFallthroughCasesInSwitch": true,
12+
"module": "esnext",
13+
"moduleResolution": "node",
14+
"resolveJsonModule": true,
15+
"isolatedModules": true,
16+
"noEmit": true,
17+
"jsx": "react"
18+
},
19+
"include": ["src", "tests"]
20+
}

0 commit comments

Comments
 (0)