Skip to content

test(nextjs): Migrate Next SDK's client side tests to Playwright. #6718

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jan 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 67 additions & 3 deletions packages/integration-tests/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Page, Request } from '@playwright/test';
import type { ReplayContainer } from '@sentry/replay/build/npm/types/types';
import type { Event, EventEnvelopeHeaders } from '@sentry/types';
import type { EnvelopeItemType, Event, EventEnvelopeHeaders } from '@sentry/types';

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

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

export const getEnvelopeType = (request: Request | null): EnvelopeItemType => {
const envelope = request?.postData() || '';

return (envelope.split('\n').map(line => JSON.parse(line))[1] as Record<string, unknown>).type as EnvelopeItemType;
};

export const countEnvelopes = async (
page: Page,
options?: {
url?: string;
timeout?: number;
envelopeType: EnvelopeItemType | EnvelopeItemType[];
},
): Promise<number> => {
const countPromise = new Promise<number>((resolve, reject) => {
let reqCount = 0;

const requestHandler = (request: Request): void => {
if (envelopeUrlRegex.test(request.url())) {
try {
if (options?.envelopeType) {
const envelopeTypeArray = options
? typeof options.envelopeType === 'string'
? [options.envelopeType]
: options.envelopeType || (['event'] as EnvelopeItemType[])
: (['event'] as EnvelopeItemType[]);

if (envelopeTypeArray.includes(getEnvelopeType(request))) {
reqCount++;
}
}
} catch (e) {
reject(e);
}
}
};

page.on('request', requestHandler);

setTimeout(() => {
page.off('request', requestHandler);
resolve(reqCount);
}, options?.timeout || 1000);
});

if (options?.url) {
await page.goto(options.url);
}

return countPromise;
};

/**
* Run script at the given path inside the test environment.
*
Expand Down Expand Up @@ -76,6 +128,7 @@ async function getMultipleRequests<T>(
options?: {
url?: string;
timeout?: number;
envelopeType?: EnvelopeItemType | EnvelopeItemType[];
},
): Promise<T[]> {
const requests: Promise<T[]> = new Promise((resolve, reject) => {
Expand All @@ -86,6 +139,18 @@ async function getMultipleRequests<T>(
function requestHandler(request: Request): void {
if (urlRgx.test(request.url())) {
try {
if (options?.envelopeType) {
const envelopeTypeArray = options
? typeof options.envelopeType === 'string'
? [options.envelopeType]
: options.envelopeType || (['event'] as EnvelopeItemType[])
: (['event'] as EnvelopeItemType[]);

if (!envelopeTypeArray.includes(getEnvelopeType(request))) {
return;
}
}

reqCount--;
requestData.push(requestParser(request));

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

Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module.exports = {
parserOptions: {
jsx: true,
},
ignorePatterns: ['test/integration/**'],
ignorePatterns: ['test/integration/**', 'playwright.config.ts'],
extends: ['../../.eslintrc.js'],
rules: {
'@sentry-internal/sdk/no-optional-chaining': 'off',
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ module.exports = {
...baseConfig,
// This prevents the build tests from running when unit tests run. (If they do, they fail, because the build being
// tested hasn't necessarily run yet.)
testPathIgnorePatterns: ['<rootDir>/test/buildProcess/'],
testPathIgnorePatterns: ['<rootDir>/test/buildProcess/', '<rootDir>/test/integration/'],
};
8 changes: 7 additions & 1 deletion packages/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,13 @@
"test:all": "run-s test:unit test:integration test:build",
"test:build": "yarn ts-node test/buildProcess/runTest.ts",
"test:unit": "jest",
"test:integration": "test/run-integration-tests.sh && yarn test:types",
"test:integration": "./test/run-integration-tests.sh && yarn test:types",
"test:integration:ci": "run-s test:integration:clean test:integration:client:ci test:integration:server",
"test:integration:prepare": "(cd test/integration && yarn build && yarn start)",
"test:integration:clean": "(cd test/integration && rimraf .cache node_modules build)",
"test:integration:client": "yarn playwright install-deps && yarn playwright test test/integration/test/client/",
"test:integration:client:ci": "yarn test:integration:client --browser='all' --reporter='line'",
"test:integration:server": "export NODE_OPTIONS='--stack-trace-limit=25' && jest --config=test/integration/jest.config.js test/integration/test/server/",
"test:types": "cd test/types && yarn test",
"test:watch": "jest --watch",
"vercel:branch": "source vercel/set-up-branch-for-test-app-use.sh",
Expand Down
16 changes: 16 additions & 0 deletions packages/nextjs/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { PlaywrightTestConfig } from '@playwright/test';

const config: PlaywrightTestConfig = {
retries: 2,
timeout: 12000,
use: {
baseURL: 'http://localhost:3000',
},
workers: 3,
webServer: {
command: 'yarn test:integration:prepare',
port: 3000,
},
};

export default config;
6 changes: 3 additions & 3 deletions packages/nextjs/test/integration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"dev": "next",
"build": "next build",
"predebug": "source ../integration_test_utils.sh && link_monorepo_packages '../../..' && yarn build",
"start": "next start"
"start": "next start",
"pretest": "run-s build",
"test": "playwright test"
},
"dependencies": {
"@sentry/nextjs": "file:../../",
Expand All @@ -15,11 +17,9 @@
},
"devDependencies": {
"@types/node": "^15.3.1",
"@types/puppeteer": "^5.4.3",
"@types/react": "17.0.47",
"@types/react-dom": "17.0.17",
"nock": "^13.1.0",
"puppeteer": "^9.1.1",
"typescript": "^4.2.4",
"yargs": "^16.2.0"
},
Expand Down
41 changes: 0 additions & 41 deletions packages/nextjs/test/integration/test/client.js

This file was deleted.

21 changes: 0 additions & 21 deletions packages/nextjs/test/integration/test/client/errorClick.js

This file was deleted.

17 changes: 17 additions & 0 deletions packages/nextjs/test/integration/test/client/errorClick.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { getMultipleSentryEnvelopeRequests } from './utils/helpers';
import { test, expect } from '@playwright/test';
import { Event } from '@sentry/types';

test('should capture error triggered on click', async ({ page }) => {
await page.goto('/errorClick');

const [_, events] = await Promise.all([
page.click('button'),
getMultipleSentryEnvelopeRequests<Event>(page, 1, { envelopeType: 'event' }),
Comment on lines +9 to +10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential race condition

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've done this before with no issues:

await Promise.all([page.click('#throw-error'), getFirstSentryEnvelopeRequest<SessionContext>(page)])

We can adjust this if it is causing flakes.

]);

expect(events[0].exception?.values?.[0]).toMatchObject({
type: 'Error',
value: 'Sentry Frontend Error',
});
});
19 changes: 0 additions & 19 deletions packages/nextjs/test/integration/test/client/errorGlobal.js

This file was deleted.

12 changes: 12 additions & 0 deletions packages/nextjs/test/integration/test/client/errorGlobal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { getMultipleSentryEnvelopeRequests } from './utils/helpers';
import { test, expect } from '@playwright/test';
import { Event } from '@sentry/types';

test('should capture a globally triggered event', async ({ page }) => {
const event = await getMultipleSentryEnvelopeRequests<Event>(page, 1, { url: '/crashed', envelopeType: 'event' });

expect(event[0].exception?.values?.[0]).toMatchObject({
type: 'Error',
value: 'Crashed',
});
});

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { test, expect } from '@playwright/test';

// This test verifies that a faulty configuration of `getInitialProps` in `_app` will not cause our
// auto - wrapping / instrumentation to throw an error.
// See `_app.tsx` for more information.

test('should not fail auto-wrapping when `getInitialProps` configuration is faulty.', async ({ page }) => {
await page.goto('/faultyAppGetInitialProps');

const serverErrorText = await page.$('//*[contains(text(), "Internal Server Error")]');

expect(serverErrorText).toBeFalsy();
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const assert = require('assert');
import { test, expect } from '@playwright/test';

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

await page.click('button');

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

assert(dialogScriptSrc.startsWith('https://dsn.ingest.sentry.io/api/embed/error-page/?'));
};
expect(dialogScriptSrc).toMatch(/^https:\/\/dsn\.ingest\.sentry\.io\/api\/embed\/error-page\/\?.*/);
});
27 changes: 0 additions & 27 deletions packages/nextjs/test/integration/test/client/sessionCrashed.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { countEnvelopes, getMultipleSentryEnvelopeRequests } from './utils/helpers';
import { test, expect } from '@playwright/test';
import { Session } from '@sentry/types';

test('should report crashed sessions', async ({ page }) => {
const event = await getMultipleSentryEnvelopeRequests<Session>(page, 2, { url: '/crashed', envelopeType: 'session' });

expect(event[0]).toMatchObject({
init: true,
status: 'ok',
errors: 0,
});

expect(event[1]).toMatchObject({
init: false,
status: 'crashed',
errors: 1,
});

expect(await countEnvelopes(page, { url: '/crashed', envelopeType: 'session' })).toBe(2);
});
Loading