Skip to content

Commit f1afb1f

Browse files
authored
test(nextjs): Add NextJS client-side E2E tests (#6669)
1 parent 2662401 commit f1afb1f

23 files changed

+1343
-2
lines changed

packages/e2e-tests/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ fields:
6666

6767
- The `buildCommand` command runs only once before any of the tests and is supposed to build the test application. If
6868
this command returns a non-zero exit code, it counts as a failed test and the test application's tests are not run. In
69-
the example above, we use the `--pure-lockfile` flag to install depencies without modifiying the lockfile so that
69+
the example above, we use the `--pure-lockfile` flag to install dependencies without modifiying the lockfile so that
7070
there aren't any changes in the git worktree after running the tests.
7171
- The `testCommand` command is supposed to run tests on the test application. If the configured command returns a
7272
non-zero exit code, it counts as a failed test.
@@ -107,7 +107,7 @@ A standardized frontend test application has the following features:
107107
`standard-frontend-nextjs`.
108108
- A page at path `/`
109109
- Having a `<input type="button" id="exception-button">` that captures an Exception when clicked. The returned
110-
`eventId` from the `Sentry.captureException()` call must be written to `window.capturedExceptionId`. It doesn not
110+
`eventId` from the `Sentry.captureException()` call must be written to `window.capturedExceptionId`. It does not
111111
matter what the captured error looks like.
112112
- Having an link with `id="navigation"` that navigates to `/user/5`. It doesn't have to be an `<a>` tag, for example
113113
if a framework has another way of doing routing, the important part is that the element to click for navigation has

packages/e2e-tests/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"devDependencies": {
2222
"@types/glob": "8.0.0",
2323
"@types/node": "^14.6.4",
24+
"dotenv": "16.0.3",
2425
"glob": "8.0.3",
2526
"ts-node": "10.9.1",
2627
"typescript": "3.8.3",

packages/e2e-tests/run.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
/* eslint-disable max-lines */
22
/* eslint-disable no-console */
33
import * as childProcess from 'child_process';
4+
import * as dotenv from 'dotenv';
45
import * as fs from 'fs';
56
import * as glob from 'glob';
67
import * as path from 'path';
78

9+
// Load environment variables from .env file locally
10+
dotenv.config();
11+
812
const repositoryRoot = path.resolve(__dirname, '../..');
913

1014
const TEST_REGISTRY_CONTAINER_NAME = 'verdaccio-e2e-test-registry';
@@ -51,6 +55,7 @@ if (missingEnvVar) {
5155

5256
const envVarsToInject = {
5357
REACT_APP_E2E_TEST_DSN: process.env.E2E_TEST_DSN,
58+
NEXT_PUBLIC_E2E_TEST_DSN: process.env.E2E_TEST_DSN,
5459
};
5560

5661
// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#grouping-log-lines
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
.pnpm-debug.log*
27+
28+
# local env files
29+
.env*.local
30+
31+
# vercel
32+
.vercel
33+
34+
# typescript
35+
*.tsbuildinfo
36+
next-env.d.ts
37+
38+
!*.d.ts
39+
40+
# Sentry
41+
.sentryclirc
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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
interface Window {
2+
recordedTransactions?: string[];
3+
capturedExceptionId?: string;
4+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/// <reference types="next" />
2+
/// <reference types="next/image-types/global" />
3+
4+
// NOTE: This file should not be edited
5+
// see https://nextjs.org/docs/basic-features/typescript for more information.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// This file sets a custom webpack configuration to use your Next.js app
2+
// with Sentry.
3+
// https://nextjs.org/docs/api-reference/next.config.js/introduction
4+
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
5+
6+
const { withSentryConfig } = require('@sentry/nextjs');
7+
8+
const moduleExports = {
9+
// Your existing module.exports
10+
11+
sentry: {
12+
// Use `hidden-source-map` rather than `source-map` as the Webpack `devtool`
13+
// for client-side builds. (This will be the default starting in
14+
// `@sentry/nextjs` version 8.0.0.) See
15+
// https://webpack.js.org/configuration/devtool/ and
16+
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#use-hidden-source-map
17+
// for more information.
18+
hideSourceMaps: true,
19+
},
20+
};
21+
22+
const sentryWebpackPluginOptions = {
23+
// Additional config options for the Sentry Webpack plugin. Keep in mind that
24+
// the following options are set automatically, and overriding them is not
25+
// recommended:
26+
// release, url, org, project, authToken, configFile, stripPrefix,
27+
// urlPrefix, include, ignore
28+
29+
silent: true, // Suppresses all logs
30+
// For all available options, see:
31+
// https://github.com/getsentry/sentry-webpack-plugin#options.
32+
33+
// We're not testing source map uploads at the moment.
34+
dryRun: true,
35+
};
36+
37+
// Make sure adding Sentry options is the last code to run before exporting, to
38+
// ensure that your source maps include changes from all other Webpack plugins
39+
module.exports = withSentryConfig(moduleExports, sentryWebpackPluginOptions);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "create-next-app",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"dev": "next dev",
7+
"build": "next build",
8+
"start": "next start",
9+
"lint": "next lint",
10+
"test": "TEST_MODE=build playwright test",
11+
"test:dev": "TEST_MODE=dev playwright test"
12+
},
13+
"dependencies": {
14+
"@next/font": "13.0.7",
15+
"@sentry/nextjs": "*",
16+
"@types/node": "18.11.17",
17+
"@types/react": "18.0.26",
18+
"@types/react-dom": "18.0.9",
19+
"next": "13.0.7",
20+
"react": "18.2.0",
21+
"react-dom": "18.2.0",
22+
"typescript": "4.9.4"
23+
},
24+
"devDependencies": {
25+
"@playwright/test": "^1.27.1"
26+
}
27+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { AppProps } from 'next/app';
2+
3+
export default function App({ Component, pageProps }: AppProps) {
4+
return <Component {...pageProps} />;
5+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Html, Head, Main, NextScript } from 'next/document';
2+
3+
export default function Document() {
4+
return (
5+
<Html lang="en">
6+
<Head />
7+
<body>
8+
<Main />
9+
<NextScript />
10+
</body>
11+
</Html>
12+
);
13+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* NOTE: This requires `@sentry/nextjs` version 7.3.0 or higher.
3+
*
4+
* NOTE: If using this with `next` version 12.2.0 or lower, uncomment the
5+
* penultimate line in `CustomErrorComponent`.
6+
*
7+
* This page is loaded by Nextjs:
8+
* - on the server, when data-fetching methods throw or reject
9+
* - on the client, when `getInitialProps` throws or rejects
10+
* - on the client, when a React lifecycle method throws or rejects, and it's
11+
* caught by the built-in Nextjs error boundary
12+
*
13+
* See:
14+
* - https://nextjs.org/docs/basic-features/data-fetching/overview
15+
* - https://nextjs.org/docs/api-reference/data-fetching/get-initial-props
16+
* - https://reactjs.org/docs/error-boundaries.html
17+
*/
18+
19+
import * as Sentry from '@sentry/nextjs';
20+
import NextErrorComponent from 'next/error';
21+
import { NextPageContext } from 'next';
22+
23+
const CustomErrorComponent = (props: { statusCode: any }) => {
24+
// If you're using a Nextjs version prior to 12.2.1, uncomment this to
25+
// compensate for https://github.com/vercel/next.js/issues/8592
26+
// Sentry.captureUnderscoreErrorException(props);
27+
28+
return <NextErrorComponent statusCode={props.statusCode} />;
29+
};
30+
31+
CustomErrorComponent.getInitialProps = async (contextData: NextPageContext) => {
32+
// In case this is running in a serverless function, await this in order to give Sentry
33+
// time to send the error before the lambda exits
34+
await Sentry.captureUnderscoreErrorException(contextData);
35+
36+
// This will contain the status code of the response
37+
return NextErrorComponent.getInitialProps(contextData);
38+
};
39+
40+
export default CustomErrorComponent;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2+
import type { NextApiRequest, NextApiResponse } from 'next';
3+
4+
type Data = {
5+
name: string;
6+
};
7+
8+
export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
9+
res.status(200).json({ name: 'John Doe' });
10+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Head from 'next/head';
2+
import Link from 'next/link';
3+
import * as Sentry from '@sentry/nextjs';
4+
5+
export default function Home() {
6+
return (
7+
<>
8+
<Head>
9+
<title>Create Next App</title>
10+
<meta name="description" content="Generated by create next app" />
11+
<meta name="viewport" content="width=device-width, initial-scale=1" />
12+
<link rel="icon" href="/favicon.ico" />
13+
</Head>
14+
<main>
15+
<input
16+
type="button"
17+
value="Capture Exception"
18+
id="exception-button"
19+
onClick={() => {
20+
const eventId = Sentry.captureException(new Error('I am an error!'));
21+
window.capturedExceptionId = eventId;
22+
}}
23+
/>
24+
<Link href="/user/5" id="navigation">
25+
navigate
26+
</Link>
27+
</main>
28+
</>
29+
);
30+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const User = () => {
2+
return <p>I am a blank page :)</p>;
3+
};
4+
5+
export default User;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { PlaywrightTestConfig } from '@playwright/test';
2+
import { devices } from '@playwright/test';
3+
4+
/**
5+
* See https://playwright.dev/docs/test-configuration.
6+
*/
7+
const config: PlaywrightTestConfig = {
8+
testDir: './tests',
9+
/* Maximum time one test can run for. */
10+
timeout: 60 * 1000,
11+
expect: {
12+
/**
13+
* Maximum time expect() should wait for the condition to be met.
14+
* For example in `await expect(locator).toHaveText();`
15+
*/
16+
timeout: 5000,
17+
},
18+
/* Run tests in files in parallel */
19+
fullyParallel: true,
20+
/* Fail the build on CI if you accidentally left test.only in the source code. */
21+
forbidOnly: !!process.env.CI,
22+
/* Retry on CI only */
23+
retries: 0,
24+
/* Opt out of parallel tests on CI. */
25+
workers: 1,
26+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
27+
reporter: 'dot',
28+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
29+
use: {
30+
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
31+
actionTimeout: 0,
32+
/* Base URL to use in actions like `await page.goto('/')`. */
33+
// baseURL: 'http://localhost:3000',
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+
// name: 'firefox',
50+
// use: {
51+
// ...devices['Desktop Firefox'],
52+
// },
53+
// },
54+
// {
55+
// name: 'webkit',
56+
// use: {
57+
// ...devices['Desktop Safari'],
58+
// },
59+
// },
60+
],
61+
62+
/* Run your local dev server before starting the tests */
63+
webServer: {
64+
command: process.env.TEST_MODE === 'build' ? 'yarn start' : 'yarn dev',
65+
port: 3000,
66+
},
67+
};
68+
69+
export default config;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// This file configures the initialization of Sentry on the browser.
2+
// The config you add here will be used whenever a page is visited.
3+
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
4+
5+
import * as Sentry from '@sentry/nextjs';
6+
7+
Sentry.init({
8+
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
9+
// Adjust this value in production, or use tracesSampler for greater control
10+
tracesSampleRate: 1.0,
11+
// ...
12+
// Note: if you want to override the automatic release value, do not set a
13+
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
14+
// that it will also get attached to your source maps
15+
});
16+
17+
Sentry.addGlobalEventProcessor(event => {
18+
if (
19+
event.type === 'transaction' &&
20+
(event.contexts?.trace?.op === 'pageload' || event.contexts?.trace?.op === 'navigation')
21+
) {
22+
const eventId = event.event_id;
23+
if (eventId) {
24+
window.recordedTransactions = window.recordedTransactions || [];
25+
window.recordedTransactions.push(eventId);
26+
}
27+
}
28+
29+
return event;
30+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// This file configures the initialization of Sentry on the server.
2+
// The config you add here will be used whenever the server handles a request.
3+
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
4+
5+
import * as Sentry from '@sentry/nextjs';
6+
7+
Sentry.init({
8+
dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
9+
// Adjust this value in production, or use tracesSampler for greater control
10+
tracesSampleRate: 1.0,
11+
// ...
12+
// Note: if you want to override the automatic release value, do not set a
13+
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
14+
// that it will also get attached to your source maps
15+
});

0 commit comments

Comments
 (0)