Skip to content

Commit d8313f3

Browse files
authored
test(remix): Add a boilerplate for Remix SDK integration tests. (#5453)
Adds an integration testing boilerplate for Remix SDK and sample tests for client and server. As the line between client and server is blurred in Remix sources, all scenarios are defined in a common Remix application, but tested separately for `client` and `server`. The server side tests uses the utilities we have already implemented for [`node-integration-tests`](https://github.com/getsentry/sentry-javascript/tree/master/packages/node-integration-tests). Needs some extra features though, such as triggering `post` requests to test `action` functions. The client side tests uses the helper utilities from our Playwright browser tests, did not need to implement `fixtures` or anything, as it seems these tests will be fairly simple.
1 parent 1d5ac33 commit d8313f3

21 files changed

+315
-2
lines changed

.github/workflows/build.yml

+35
Original file line numberDiff line numberDiff line change
@@ -571,3 +571,38 @@ jobs:
571571
run: |
572572
cd packages/node-integration-tests
573573
yarn test
574+
575+
job_remix_integration_tests:
576+
name: Remix SDK Integration Tests (${{ matrix.node }})
577+
needs: [job_get_metadata, job_build]
578+
runs-on: ubuntu-latest
579+
timeout-minutes: 10
580+
continue-on-error: true
581+
strategy:
582+
matrix:
583+
node: [14, 16, 18]
584+
steps:
585+
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
586+
uses: actions/checkout@v2
587+
with:
588+
ref: ${{ env.HEAD_COMMIT }}
589+
- name: Set up Node
590+
uses: actions/setup-node@v1
591+
with:
592+
node-version: ${{ matrix.node }}
593+
- name: Check dependency cache
594+
uses: actions/cache@v2
595+
with:
596+
path: ${{ env.CACHED_DEPENDENCY_PATHS }}
597+
key: ${{ needs.job_build.outputs.dependency_cache_key }}
598+
- name: Check build cache
599+
uses: actions/cache@v2
600+
with:
601+
path: ${{ env.CACHED_BUILD_PATHS }}
602+
key: ${{ env.BUILD_CACHE_KEY }}
603+
- name: Run integration tests
604+
env:
605+
NODE_VERSION: ${{ matrix.node }}
606+
run: |
607+
cd packages/remix
608+
yarn test:integration:ci

packages/remix/.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module.exports = {
66
parserOptions: {
77
jsx: true,
88
},
9+
ignorePatterns: ['playwright.config.ts', 'test/integration/**'],
910
extends: ['../../.eslintrc.js'],
1011
rules: {
1112
'@sentry-internal/sdk/no-async-await': 'off',

packages/remix/jest.config.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
module.exports = require('../../jest/jest.config.js');
1+
const baseConfig = require('../../jest/jest.config.js');
2+
3+
module.exports = {
4+
...baseConfig,
5+
testPathIgnorePatterns: ['<rootDir>/build/', '<rootDir>/node_modules/', '<rootDir>/test/integration/'],
6+
};

packages/remix/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@
6060
"lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish",
6161
"lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"",
6262
"test": "run-s test:unit",
63+
"test:integration": "run-s test:integration:prepare test:integration:client test:integration:server",
64+
"test:integration:ci": "run-s test:integration:prepare test:integration:client:ci test:integration:server",
65+
"test:integration:prepare": "(cd test/integration && yarn)",
66+
"test:integration:client": "yarn playwright install-deps && yarn playwright test test/integration/test/client/",
67+
"test:integration:client:ci": "yarn test:integration:client --browser='all' --reporter='line'",
68+
"test:integration:server": "jest --config=test/integration/jest.config.js test/integration/test/server/",
6369
"test:unit": "jest",
6470
"test:watch": "jest --watch"
6571
},

packages/remix/playwright.config.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { PlaywrightTestConfig } from '@playwright/test';
2+
3+
const config: PlaywrightTestConfig = {
4+
retries: 2,
5+
timeout: 12000,
6+
use: {
7+
baseURL: 'http://localhost:3000',
8+
},
9+
workers: 3,
10+
webServer: {
11+
command: '(cd test/integration/ && yarn build && yarn start)',
12+
port: 3000,
13+
},
14+
};
15+
16+
export default config;
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
node_modules
2+
3+
/.cache
4+
/build
5+
/public/build
6+
.env
7+
/test-results/
8+
/playwright-report/
9+
/playwright/.cache/
10+
yarn.lock
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { RemixBrowser, useLocation, useMatches } from '@remix-run/react';
2+
import { hydrate } from 'react-dom';
3+
import * as Sentry from '@sentry/remix';
4+
import { useEffect } from 'react';
5+
6+
Sentry.init({
7+
dsn: 'https://[email protected]/1337',
8+
tracesSampleRate: 1,
9+
integrations: [
10+
new Sentry.BrowserTracing({
11+
routingInstrumentation: Sentry.remixRouterInstrumentation(useEffect, useLocation, useMatches),
12+
}),
13+
],
14+
});
15+
16+
hydrate(<RemixBrowser />, document);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { EntryContext } from '@remix-run/node';
2+
import { RemixServer } from '@remix-run/react';
3+
import { renderToString } from 'react-dom/server';
4+
import * as Sentry from '@sentry/remix';
5+
6+
Sentry.init({
7+
dsn: 'https://[email protected]/1337',
8+
tracesSampleRate: 1,
9+
});
10+
11+
export default function handleRequest(
12+
request: Request,
13+
responseStatusCode: number,
14+
responseHeaders: Headers,
15+
remixContext: EntryContext,
16+
) {
17+
let markup = renderToString(<RemixServer context={remixContext} url={request.url} />);
18+
19+
responseHeaders.set('Content-Type', 'text/html');
20+
21+
return new Response('<!DOCTYPE html>' + markup, {
22+
status: responseStatusCode,
23+
headers: responseHeaders,
24+
});
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { MetaFunction } from '@remix-run/node';
2+
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react';
3+
import { withSentry } from '@sentry/remix';
4+
5+
export const meta: MetaFunction = () => ({
6+
charset: 'utf-8',
7+
title: 'New Remix App',
8+
viewport: 'width=device-width,initial-scale=1',
9+
});
10+
11+
function App() {
12+
return (
13+
<html lang="en">
14+
<head>
15+
<Meta />
16+
<Links />
17+
</head>
18+
<body>
19+
<Outlet />
20+
<ScrollRestoration />
21+
<Scripts />
22+
<LiveReload />
23+
</body>
24+
</html>
25+
);
26+
}
27+
28+
export default withSentry(App);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Index() {
2+
return (
3+
<div>
4+
<h1>Remix Integration Tests Home</h1>
5+
</div>
6+
);
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { json, LoaderFunction } from '@remix-run/node';
2+
import { useLoaderData } from '@remix-run/react';
3+
4+
type LoaderData = { id: string };
5+
6+
export const loader: LoaderFunction = async ({ params: { id } }) => {
7+
return json({
8+
id,
9+
});
10+
};
11+
12+
export default function LoaderJSONResponse() {
13+
const data = useLoaderData<LoaderData>();
14+
15+
return (
16+
<div>
17+
<h1>{data.id}</h1>
18+
</div>
19+
);
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const baseConfig = require('../../jest.config.js');
2+
3+
module.exports = {
4+
...baseConfig,
5+
testMatch: [`${__dirname}/test/server/**/*.test.ts`],
6+
testPathIgnorePatterns: [`${__dirname}/test/client`],
7+
detectOpenHandles: true,
8+
forceExit: true,
9+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"private": true,
3+
"sideEffects": false,
4+
"scripts": {
5+
"build": "remix build",
6+
"dev": "remix dev",
7+
"start": "remix-serve build"
8+
},
9+
"dependencies": {
10+
"@remix-run/express": "^1.6.5",
11+
"@remix-run/node": "^1.6.5",
12+
"@remix-run/react": "^1.6.5",
13+
"@remix-run/serve": "^1.6.5",
14+
"@sentry/remix": "file:../..",
15+
"react": "^17.0.2",
16+
"react-dom": "^17.0.2"
17+
},
18+
"devDependencies": {
19+
"@remix-run/dev": "^1.6.5",
20+
"@types/react": "^17.0.47",
21+
"@types/react-dom": "^17.0.17",
22+
"typescript": "^4.2.4"
23+
},
24+
"resolutions": {
25+
"@sentry/browser": "file:../../../browser",
26+
"@sentry/core": "file:../../../core",
27+
"@sentry/hub": "file:../../../hub",
28+
"@sentry/integrations": "file:../../../integrations",
29+
"@sentry/node": "file:../../../node",
30+
"@sentry/react": "file:../../../react",
31+
"@sentry/tracing": "file:../../../tracing",
32+
"@sentry/types": "file:../../../types",
33+
"@sentry/utils": "file:../../../utils"
34+
},
35+
"engines": {
36+
"node": ">=14"
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/** @type {import('@remix-run/dev').AppConfig} */
2+
module.exports = {
3+
appDirectory: 'app',
4+
assetsBuildDirectory: 'public/build',
5+
serverBuildPath: 'build/index.js',
6+
publicPath: '/build/',
7+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { getFirstSentryEnvelopeRequest } from './utils/helpers';
2+
import { test, expect } from '@playwright/test';
3+
import { Event } from '@sentry/types';
4+
5+
test('should add `pageload` transaction on load.', async ({ page }) => {
6+
const envelope = await getFirstSentryEnvelopeRequest<Event>(page, '/');
7+
8+
expect(envelope.contexts?.trace.op).toBe('pageload');
9+
expect(envelope.tags?.['routing.instrumentation']).toBe('remix-router');
10+
expect(envelope.type).toBe('transaction');
11+
expect(envelope.transaction).toBe('routes/index');
12+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from '../../../../../../integration-tests/utils/helpers';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { assertSentryTransaction, getEnvelopeRequest, runServer } from './utils/helpers';
2+
3+
describe('Remix API Loaders', () => {
4+
it('correctly instruments a Remix API loader', async () => {
5+
const baseURL = await runServer();
6+
const url = `${baseURL}/loader-json-response/123123`;
7+
const envelope = await getEnvelopeRequest(url);
8+
const transaction = envelope[2];
9+
10+
assertSentryTransaction(transaction, {
11+
spans: [
12+
{
13+
description: url,
14+
op: 'remix.server.loader',
15+
},
16+
{
17+
description: url,
18+
op: 'remix.server.documentRequest',
19+
},
20+
],
21+
});
22+
});
23+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import express from 'express';
2+
import { createRequestHandler } from '@remix-run/express';
3+
import { getPortPromise } from 'portfinder';
4+
5+
export * from '../../../../../../node-integration-tests/utils';
6+
7+
/**
8+
* Runs a test server
9+
* @returns URL
10+
*/
11+
export async function runServer(): Promise<string> {
12+
const app = express();
13+
const port = await getPortPromise();
14+
15+
app.all('*', createRequestHandler({ build: require('../../../build') }));
16+
17+
const server = app.listen(port, () => {
18+
setTimeout(() => {
19+
server.close();
20+
}, 4000);
21+
});
22+
23+
return `http://localhost:${port}`;
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
3+
"compilerOptions": {
4+
"lib": ["DOM", "DOM.Iterable", "ES2019"],
5+
"isolatedModules": true,
6+
"esModuleInterop": true,
7+
"jsx": "react-jsx",
8+
"moduleResolution": "node",
9+
"resolveJsonModule": true,
10+
"target": "ES2019",
11+
"strict": true,
12+
"allowJs": true,
13+
"forceConsistentCasingInFileNames": true,
14+
"baseUrl": ".",
15+
"paths": {
16+
"~/*": ["./app/*"]
17+
},
18+
"noEmit": true
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
4+
"include": ["test/**/*"],
5+
6+
"compilerOptions": {
7+
"types": ["node", "jest"]
8+
}
9+
}

packages/remix/tsconfig.test.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"include": ["test/**/*"],
55

66
"compilerOptions": {
7-
"types": ["node", "jest"]
7+
"types": ["node", "jest"],
8+
"esModuleInterop": true
89
}
910
}

0 commit comments

Comments
 (0)