Skip to content

Commit fd3df68

Browse files
committed
Add cloudflare-pages e2e tests.
1 parent 384fba2 commit fd3df68

26 files changed

+681
-0
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,7 @@ jobs:
872872
'generic-ts3.8',
873873
'node-experimental-fastify-app',
874874
'remix-cloudflare-workers',
875+
'remix-cloudflare-pages',
875876
]
876877
build-command:
877878
- false
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/** @type {import('eslint').Linter.Config} */
2+
module.exports = {
3+
extends: ['@remix-run/eslint-config', '@remix-run/eslint-config/node'],
4+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
node_modules
2+
3+
/.cache
4+
/functions/\[\[path\]\].js
5+
/functions/\[\[path\]\].js.map
6+
/functions/metafile.*
7+
/functions/version.txt
8+
/public/build
9+
.dev.vars
10+
env.ts
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://localhost:4873
2+
@sentry-internal:registry=http://localhost:4873
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* By default, Remix will handle hydrating your app on the client for you.
3+
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4+
* For more information, see https://remix.run/file-conventions/entry.client
5+
*/
6+
7+
import { RemixBrowser, useLocation, useMatches } from '@remix-run/react';
8+
import { startTransition, StrictMode, useEffect } from 'react';
9+
import { hydrateRoot } from 'react-dom/client';
10+
import * as Sentry from '@sentry/remix';
11+
12+
Sentry.init({
13+
dsn: window.ENV.SENTRY_DSN,
14+
integrations: [
15+
new Sentry.BrowserTracing({
16+
routingInstrumentation: Sentry.remixRouterInstrumentation(useEffect, useLocation, useMatches),
17+
}),
18+
new Sentry.Replay(),
19+
],
20+
// Performance Monitoring
21+
tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production!
22+
// Session Replay
23+
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
24+
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
25+
});
26+
27+
Sentry.addGlobalEventProcessor(event => {
28+
if (
29+
event.type === 'transaction' &&
30+
(event.contexts?.trace?.op === 'pageload' || event.contexts?.trace?.op === 'navigation')
31+
) {
32+
const eventId = event.event_id;
33+
if (eventId) {
34+
window.recordedTransactions = window.recordedTransactions || [];
35+
window.recordedTransactions.push(eventId);
36+
}
37+
}
38+
39+
return event;
40+
});
41+
42+
startTransition(() => {
43+
hydrateRoot(
44+
document,
45+
<StrictMode>
46+
<RemixBrowser />
47+
</StrictMode>,
48+
);
49+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* By default, Remix will handle generating the HTTP Response for you.
3+
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4+
* For more information, see https://remix.run/file-conventions/entry.server
5+
*/
6+
7+
import type { AppLoadContext, EntryContext, DataFunctionArgs } from '@remix-run/cloudflare';
8+
import { RemixServer } from '@remix-run/react';
9+
import isbot from 'isbot';
10+
import { renderToReadableStream } from 'react-dom/server';
11+
import * as Sentry from '@sentry/remix';
12+
import { env } from '../env';
13+
14+
Sentry.workerInit({
15+
dsn: env.SENTRY_DSN,
16+
// Performance Monitoring
17+
tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production!
18+
debug: true,
19+
});
20+
21+
export function handleError(error: unknown, { request }: DataFunctionArgs): void {
22+
Sentry.captureRemixServerException(error, 'remix.server', request);
23+
}
24+
25+
export default async function handleRequest(
26+
request: Request,
27+
responseStatusCode: number,
28+
responseHeaders: Headers,
29+
remixContext: EntryContext,
30+
loadContext: AppLoadContext,
31+
) {
32+
const body = await renderToReadableStream(<RemixServer context={remixContext} url={request.url} />, {
33+
signal: request.signal,
34+
onError(error: unknown) {
35+
// Log streaming rendering errors from inside the shell
36+
console.error(error);
37+
responseStatusCode = 500;
38+
},
39+
});
40+
41+
if (isbot(request.headers.get('user-agent'))) {
42+
await body.allReady;
43+
}
44+
45+
responseHeaders.set('Content-Type', 'text/html');
46+
return new Response(body, {
47+
headers: responseHeaders,
48+
status: responseStatusCode,
49+
});
50+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { json, LinksFunction } from '@remix-run/cloudflare';
2+
import { cssBundleHref } from '@remix-run/css-bundle';
3+
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from '@remix-run/react';
4+
import { withSentry } from '@sentry/remix';
5+
import { env } from '../env';
6+
7+
export const links: LinksFunction = () => [...(cssBundleHref ? [{ rel: 'stylesheet', href: cssBundleHref }] : [])];
8+
9+
export const loader = () => {
10+
return json({
11+
ENV: {
12+
SENTRY_DSN: env.SENTRY_DSN,
13+
},
14+
});
15+
};
16+
17+
function App() {
18+
const { ENV } = useLoaderData();
19+
20+
return (
21+
<html lang="en">
22+
<head>
23+
<meta charSet="utf-8" />
24+
<meta name="viewport" content="width=device-width, initial-scale=1" />
25+
<script
26+
dangerouslySetInnerHTML={{
27+
__html: `window.ENV = ${JSON.stringify(ENV)}`,
28+
}}
29+
/>
30+
<Meta />
31+
<Links />
32+
</head>
33+
<body>
34+
<Outlet />
35+
<ScrollRestoration />
36+
<Scripts />
37+
<LiveReload />
38+
</body>
39+
</html>
40+
);
41+
}
42+
43+
export default withSentry(App);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as Sentry from '@sentry/remix';
2+
import { Link, useRouteError } from '@remix-run/react';
3+
4+
export default function Index() {
5+
return (
6+
<div>
7+
<input
8+
type="button"
9+
value="Capture Exception"
10+
id="exception-button"
11+
onClick={() => {
12+
const eventId = Sentry.captureException(new Error('I am an error!'));
13+
window.capturedExceptionId = eventId;
14+
}}
15+
/>
16+
<Link to="/user/5" id="navigation">
17+
navigate
18+
</Link>
19+
</div>
20+
);
21+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useRouteError } from '@remix-run/react';
2+
import { captureRemixErrorBoundaryError } from '@sentry/remix';
3+
import { useState } from 'react';
4+
5+
export function ErrorBoundary() {
6+
const error = useRouteError();
7+
const eventId = captureRemixErrorBoundaryError(error);
8+
9+
return (
10+
<div>
11+
<span>ErrorBoundary Error</span>
12+
<span id="event-id">{eventId}</span>
13+
</div>
14+
);
15+
}
16+
17+
export default function ErrorBoundaryCapture() {
18+
const [count, setCount] = useState(0);
19+
20+
if (count > 0) {
21+
throw new Error('Sentry React Component Error');
22+
} else {
23+
setTimeout(() => setCount(count + 1), 0);
24+
}
25+
26+
return <div>{count}</div>;
27+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { LoaderFunction } from '@remix-run/node';
2+
import { useLoaderData } from '@remix-run/react';
3+
4+
export const loader: LoaderFunction = async ({ params: { id } }) => {
5+
if (id === '-1') {
6+
throw new Error('Unexpected Server Error');
7+
}
8+
9+
return null;
10+
};
11+
12+
export default function LoaderError() {
13+
const data = useLoaderData();
14+
15+
return (
16+
<div>
17+
<h1>{data && data.test ? data.test : 'Not Found'}</h1>
18+
</div>
19+
);
20+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { json } from '@remix-run/cloudflare';
2+
import { useLoaderData } from '@remix-run/react';
3+
import * as Sentry from '@sentry/remix';
4+
5+
export function loader() {
6+
const id = Sentry.captureException(new Error('Sentry Server Error'));
7+
8+
return json({ id });
9+
}
10+
11+
export default function ServerError() {
12+
const { id } = useLoaderData();
13+
14+
return (
15+
<div>
16+
<pre id="event-id">{id}</pre>
17+
</div>
18+
);
19+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function User() {
2+
return <div>I am a blank page</div>;
3+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# export environment variables from .env file
2+
3+
# exit if any command fails
4+
set -e
5+
6+
# create environment variables file
7+
cat >./env.ts <<EOF
8+
export const env = {
9+
SENTRY_DSN: '${E2E_TEST_DSN}',
10+
};
11+
EOF
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
interface Window {
2+
recordedTransactions?: string[];
3+
capturedExceptionId?: string;
4+
ENV: {
5+
SENTRY_DSN: string;
6+
};
7+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "remix-cloudflare",
3+
"private": true,
4+
"sideEffects": false,
5+
"type": "module",
6+
"scripts": {
7+
"build": "remix build",
8+
"dev": "remix dev --manual -c \"npm run start\"",
9+
"start": "wrangler pages dev ./public --compatibility-date=2023-06-21",
10+
"typecheck": "tsc",
11+
"pages:deploy": "npm run build && wrangler pages deploy ./public",
12+
"test:prepare": "./createEnvFile.sh",
13+
"test:build": "pnpm install && npx playwright install && pnpm test:prepare && pnpm build",
14+
"test:assert": "pnpm playwright test"
15+
},
16+
"dependencies": {
17+
"@sentry/remix": "latest || *",
18+
"@remix-run/cloudflare": "2.0.0",
19+
"@remix-run/cloudflare-pages": "2.0.0",
20+
"@remix-run/css-bundle": "2.0.0",
21+
"@remix-run/react": "2.0.0",
22+
"isbot": "^3.6.8",
23+
"react": "^18.2.0",
24+
"react-dom": "^18.2.0"
25+
},
26+
"devDependencies": {
27+
"@cloudflare/workers-types": "^4.20230518.0",
28+
"@playwright/test": "^1.36.2",
29+
"@remix-run/dev": "2.0.0",
30+
"@remix-run/eslint-config": "^2.0.1",
31+
"@types/react": "^18.2.20",
32+
"@types/react-dom": "^18.2.7",
33+
"eslint": "^8.38.0",
34+
"esm-loader-typescript": "1.0.5",
35+
"typescript": "^5.1.0",
36+
"wrangler": "^3.12.0"
37+
},
38+
"engines": {
39+
"node": ">=18.0.0"
40+
}
41+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { PlaywrightTestConfig } from '@playwright/test';
2+
import { devices } from '@playwright/test';
3+
4+
const port = 3030;
5+
6+
/**
7+
* See https://playwright.dev/docs/test-configuration.
8+
*/
9+
const config: PlaywrightTestConfig = {
10+
testDir: './tests',
11+
/* Maximum time one test can run for. */
12+
timeout: 60 * 1000,
13+
expect: {
14+
/**
15+
* Maximum time expect() should wait for the condition to be met.
16+
* For example in `await expect(locator).toHaveText();`
17+
*/
18+
timeout: 5000,
19+
},
20+
/* Run tests in files in parallel */
21+
fullyParallel: true,
22+
/* Fail the build on CI if you accidentally left test.only in the source code. */
23+
forbidOnly: !!process.env.CI,
24+
/* Retry on CI only */
25+
retries: 0,
26+
/* Opt out of parallel tests on CI. */
27+
workers: 1,
28+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
29+
reporter: 'list',
30+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
31+
use: {
32+
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
33+
actionTimeout: 0,
34+
35+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
36+
trace: 'on-first-retry',
37+
},
38+
39+
/* Configure projects for major browsers */
40+
projects: [
41+
{
42+
name: 'chromium',
43+
use: {
44+
...devices['Desktop Chrome'],
45+
},
46+
},
47+
// For now we only test Chrome!
48+
],
49+
50+
/* Run your local dev server before starting the tests */
51+
webServer: {
52+
command: `pnpm start --port=${port}`,
53+
port,
54+
},
55+
};
56+
57+
export default config;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/favicon.ico
2+
Cache-Control: public, max-age=3600, s-maxage=3600
3+
/build/*
4+
Cache-Control: public, max-age=31536000, immutable
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"version": 1,
3+
"include": ["/*"],
4+
"exclude": ["/favicon.ico", "/build/*"]
5+
}
Binary file not shown.

0 commit comments

Comments
 (0)