Skip to content

Commit 59e51bd

Browse files
authored
test(nextjs): Migrate Next SDK's client side tests to Playwright. (#6718)
1 parent ea61856 commit 59e51bd

38 files changed

+501
-573
lines changed

packages/integration-tests/utils/helpers.ts

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Page, Request } from '@playwright/test';
22
import type { ReplayContainer } from '@sentry/replay/build/npm/types/types';
3-
import type { Event, EventEnvelopeHeaders } from '@sentry/types';
3+
import type { EnvelopeItemType, Event, EventEnvelopeHeaders } from '@sentry/types';
44

55
const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//;
66

@@ -26,6 +26,58 @@ export const envelopeHeaderRequestParser = (request: Request | null): EventEnvel
2626
return envelope.split('\n').map(line => JSON.parse(line))[0];
2727
};
2828

29+
export const getEnvelopeType = (request: Request | null): EnvelopeItemType => {
30+
const envelope = request?.postData() || '';
31+
32+
return (envelope.split('\n').map(line => JSON.parse(line))[1] as Record<string, unknown>).type as EnvelopeItemType;
33+
};
34+
35+
export const countEnvelopes = async (
36+
page: Page,
37+
options?: {
38+
url?: string;
39+
timeout?: number;
40+
envelopeType: EnvelopeItemType | EnvelopeItemType[];
41+
},
42+
): Promise<number> => {
43+
const countPromise = new Promise<number>((resolve, reject) => {
44+
let reqCount = 0;
45+
46+
const requestHandler = (request: Request): void => {
47+
if (envelopeUrlRegex.test(request.url())) {
48+
try {
49+
if (options?.envelopeType) {
50+
const envelopeTypeArray = options
51+
? typeof options.envelopeType === 'string'
52+
? [options.envelopeType]
53+
: options.envelopeType || (['event'] as EnvelopeItemType[])
54+
: (['event'] as EnvelopeItemType[]);
55+
56+
if (envelopeTypeArray.includes(getEnvelopeType(request))) {
57+
reqCount++;
58+
}
59+
}
60+
} catch (e) {
61+
reject(e);
62+
}
63+
}
64+
};
65+
66+
page.on('request', requestHandler);
67+
68+
setTimeout(() => {
69+
page.off('request', requestHandler);
70+
resolve(reqCount);
71+
}, options?.timeout || 1000);
72+
});
73+
74+
if (options?.url) {
75+
await page.goto(options.url);
76+
}
77+
78+
return countPromise;
79+
};
80+
2981
/**
3082
* Run script at the given path inside the test environment.
3183
*
@@ -76,6 +128,7 @@ async function getMultipleRequests<T>(
76128
options?: {
77129
url?: string;
78130
timeout?: number;
131+
envelopeType?: EnvelopeItemType | EnvelopeItemType[];
79132
},
80133
): Promise<T[]> {
81134
const requests: Promise<T[]> = new Promise((resolve, reject) => {
@@ -86,6 +139,18 @@ async function getMultipleRequests<T>(
86139
function requestHandler(request: Request): void {
87140
if (urlRgx.test(request.url())) {
88141
try {
142+
if (options?.envelopeType) {
143+
const envelopeTypeArray = options
144+
? typeof options.envelopeType === 'string'
145+
? [options.envelopeType]
146+
: options.envelopeType || (['event'] as EnvelopeItemType[])
147+
: (['event'] as EnvelopeItemType[]);
148+
149+
if (!envelopeTypeArray.includes(getEnvelopeType(request))) {
150+
return;
151+
}
152+
}
153+
89154
reqCount--;
90155
requestData.push(requestParser(request));
91156

@@ -127,11 +192,10 @@ async function getMultipleSentryEnvelopeRequests<T>(
127192
options?: {
128193
url?: string;
129194
timeout?: number;
195+
envelopeType?: EnvelopeItemType | EnvelopeItemType[];
130196
},
131197
requestParser: (req: Request) => T = envelopeRequestParser as (req: Request) => T,
132198
): Promise<T[]> {
133-
// TODO: This is not currently checking the type of envelope, just casting for now.
134-
// We can update this to include optional type-guarding when we have types for Envelope.
135199
return getMultipleRequests<T>(page, count, envelopeUrlRegex, requestParser, options) as Promise<T[]>;
136200
}
137201

packages/nextjs/.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ module.exports = {
66
parserOptions: {
77
jsx: true,
88
},
9-
ignorePatterns: ['test/integration/**'],
9+
ignorePatterns: ['test/integration/**', 'playwright.config.ts'],
1010
extends: ['../../.eslintrc.js'],
1111
rules: {
1212
'@sentry-internal/sdk/no-optional-chaining': 'off',

packages/nextjs/jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ module.exports = {
44
...baseConfig,
55
// This prevents the build tests from running when unit tests run. (If they do, they fail, because the build being
66
// tested hasn't necessarily run yet.)
7-
testPathIgnorePatterns: ['<rootDir>/test/buildProcess/'],
7+
testPathIgnorePatterns: ['<rootDir>/test/buildProcess/', '<rootDir>/test/integration/'],
88
};

packages/nextjs/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,13 @@
6767
"test:all": "run-s test:unit test:integration test:build",
6868
"test:build": "yarn ts-node test/buildProcess/runTest.ts",
6969
"test:unit": "jest",
70-
"test:integration": "test/run-integration-tests.sh && yarn test:types",
70+
"test:integration": "./test/run-integration-tests.sh && yarn test:types",
71+
"test:integration:ci": "run-s test:integration:clean test:integration:client:ci test:integration:server",
72+
"test:integration:prepare": "(cd test/integration && yarn build && yarn start)",
73+
"test:integration:clean": "(cd test/integration && rimraf .cache node_modules build)",
74+
"test:integration:client": "yarn playwright install-deps && yarn playwright test test/integration/test/client/",
75+
"test:integration:client:ci": "yarn test:integration:client --browser='all' --reporter='line'",
76+
"test:integration:server": "export NODE_OPTIONS='--stack-trace-limit=25' && jest --config=test/integration/jest.config.js test/integration/test/server/",
7177
"test:types": "cd test/types && yarn test",
7278
"test:watch": "jest --watch",
7379
"vercel:branch": "source vercel/set-up-branch-for-test-app-use.sh",

packages/nextjs/playwright.config.ts

Lines changed: 16 additions & 0 deletions
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: 'yarn test:integration:prepare',
12+
port: 3000,
13+
},
14+
};
15+
16+
export default config;

packages/nextjs/test/integration/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"dev": "next",
66
"build": "next build",
77
"predebug": "source ../integration_test_utils.sh && link_monorepo_packages '../../..' && yarn build",
8-
"start": "next start"
8+
"start": "next start",
9+
"pretest": "run-s build",
10+
"test": "playwright test"
911
},
1012
"dependencies": {
1113
"@sentry/nextjs": "file:../../",
@@ -15,11 +17,9 @@
1517
},
1618
"devDependencies": {
1719
"@types/node": "^15.3.1",
18-
"@types/puppeteer": "^5.4.3",
1920
"@types/react": "17.0.47",
2021
"@types/react-dom": "17.0.17",
2122
"nock": "^13.1.0",
22-
"puppeteer": "^9.1.1",
2323
"typescript": "^4.2.4",
2424
"yargs": "^16.2.0"
2525
},

packages/nextjs/test/integration/test/client.js

Lines changed: 0 additions & 41 deletions
This file was deleted.

packages/nextjs/test/integration/test/client/errorClick.js

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { getMultipleSentryEnvelopeRequests } from './utils/helpers';
2+
import { test, expect } from '@playwright/test';
3+
import { Event } from '@sentry/types';
4+
5+
test('should capture error triggered on click', async ({ page }) => {
6+
await page.goto('/errorClick');
7+
8+
const [_, events] = await Promise.all([
9+
page.click('button'),
10+
getMultipleSentryEnvelopeRequests<Event>(page, 1, { envelopeType: 'event' }),
11+
]);
12+
13+
expect(events[0].exception?.values?.[0]).toMatchObject({
14+
type: 'Error',
15+
value: 'Sentry Frontend Error',
16+
});
17+
});

packages/nextjs/test/integration/test/client/errorGlobal.js

Lines changed: 0 additions & 19 deletions
This file was deleted.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { getMultipleSentryEnvelopeRequests } from './utils/helpers';
2+
import { test, expect } from '@playwright/test';
3+
import { Event } from '@sentry/types';
4+
5+
test('should capture a globally triggered event', async ({ page }) => {
6+
const event = await getMultipleSentryEnvelopeRequests<Event>(page, 1, { url: '/crashed', envelopeType: 'event' });
7+
8+
expect(event[0].exception?.values?.[0]).toMatchObject({
9+
type: 'Error',
10+
value: 'Crashed',
11+
});
12+
});

packages/nextjs/test/integration/test/client/faultyAppGetInitialPropsConfiguration.js

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
// This test verifies that a faulty configuration of `getInitialProps` in `_app` will not cause our
4+
// auto - wrapping / instrumentation to throw an error.
5+
// See `_app.tsx` for more information.
6+
7+
test('should not fail auto-wrapping when `getInitialProps` configuration is faulty.', async ({ page }) => {
8+
await page.goto('/faultyAppGetInitialProps');
9+
10+
const serverErrorText = await page.$('//*[contains(text(), "Internal Server Error")]');
11+
12+
expect(serverErrorText).toBeFalsy();
13+
});
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
const assert = require('assert');
1+
import { test, expect } from '@playwright/test';
22

3-
module.exports = async ({ page, url }) => {
4-
await page.goto(`${url}/reportDialog`);
3+
test('should show a dialog', async ({ page }) => {
4+
await page.goto('/reportDialog');
55

66
await page.click('button');
77

@@ -10,5 +10,5 @@ module.exports = async ({ page, url }) => {
1010
const dialogScript = await page.waitForSelector(dialogScriptSelector, { state: 'attached' });
1111
const dialogScriptSrc = await (await dialogScript.getProperty('src')).jsonValue();
1212

13-
assert(dialogScriptSrc.startsWith('https://dsn.ingest.sentry.io/api/embed/error-page/?'));
14-
};
13+
expect(dialogScriptSrc).toMatch(/^https:\/\/dsn\.ingest\.sentry\.io\/api\/embed\/error-page\/\?.*/);
14+
});

packages/nextjs/test/integration/test/client/sessionCrashed.js

Lines changed: 0 additions & 27 deletions
This file was deleted.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { countEnvelopes, getMultipleSentryEnvelopeRequests } from './utils/helpers';
2+
import { test, expect } from '@playwright/test';
3+
import { Session } from '@sentry/types';
4+
5+
test('should report crashed sessions', async ({ page }) => {
6+
const event = await getMultipleSentryEnvelopeRequests<Session>(page, 2, { url: '/crashed', envelopeType: 'session' });
7+
8+
expect(event[0]).toMatchObject({
9+
init: true,
10+
status: 'ok',
11+
errors: 0,
12+
});
13+
14+
expect(event[1]).toMatchObject({
15+
init: false,
16+
status: 'crashed',
17+
errors: 1,
18+
});
19+
20+
expect(await countEnvelopes(page, { url: '/crashed', envelopeType: 'session' })).toBe(2);
21+
});

0 commit comments

Comments
 (0)