Skip to content

Commit

Permalink
test(browser): Port unhandledrejection tests to playwright (#11758)
Browse files Browse the repository at this point in the history
ref #11084

This test ports
`packages/browser/test/integration/suites/onunhandledrejection.js`
playwright. Because of the same limitations as outlined with the on
error tests #11666, I
had to use calls to `window.onunhandledrejection` to simulate these
tests instead of just using `Promise.reject` to test the handler.

#11678 tracks being
able to fix this so we can avoid directly calling
`window.onunhandledrejection` to test.

As `onunhandledrejection.js` was the last suite to use the old
integration tests, I fully removed that code and the corresponding GH
action workflow. I also removed the monorepo deps on `karma`, `chai` and
`sinon`. Extremely satisfying.
  • Loading branch information
AbhiPrasad authored Apr 24, 2024
1 parent de98428 commit 3eea537
Show file tree
Hide file tree
Showing 50 changed files with 458 additions and 4,892 deletions.
35 changes: 0 additions & 35 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -768,40 +768,6 @@ jobs:
name: playwright-traces
path: dev-packages/browser-integration-tests/test-results

job_browser_integration_tests:
name: Browser (${{ matrix.browser }}) Tests
needs: [job_get_metadata, job_build]
if: needs.job_get_metadata.outputs.changed_browser == 'true' || github.event_name != 'pull_request'
runs-on: ubuntu-20.04
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
browser:
- ChromeHeadless
- FirefoxHeadless
- WebkitHeadless
steps:
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
uses: actions/checkout@v4
with:
ref: ${{ env.HEAD_COMMIT }}
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version-file: 'package.json'
- name: Restore caches
uses: ./.github/actions/restore-cache
env:
DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }}
- name: Run integration tests
env:
KARMA_BROWSER: ${{ matrix.browser }}
run: |
cd packages/browser
[[ $KARMA_BROWSER == WebkitHeadless ]] && yarn run playwright install-deps webkit
yarn test:integration
job_browser_build_tests:
name: Browser Build Tests
needs: [job_get_metadata, job_build]
Expand Down Expand Up @@ -1236,7 +1202,6 @@ jobs:
job_nextjs_integration_test,
job_node_integration_tests,
job_browser_playwright_tests,
job_browser_integration_tests,
job_browser_loader_tests,
job_remix_integration_tests,
job_e2e_tests,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
function run() {
// this isn't how it happens in real life, in that the promise and reason
// values come from an actual PromiseRejectionEvent, but it's enough to test
// how the SDK handles the structure
window.dispatchEvent(
new CustomEvent('unhandledrejection', {
detail: {
promise: new Promise(function () {}),
// we're testing with an error here but it could be anything - really
// all we're testing is that it gets dug out correctly
reason: new Error('promiseError'),
},
}),
);
}

run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';

import { sentryTest } from '../../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';

// something, somewhere, (likely a browser extension) effectively casts PromiseRejectionEvents
// to CustomEvents, moving the `promise` and `reason` attributes of the PRE into
// the CustomEvent's `detail` attribute, since they're not part of CustomEvent's spec
// see https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent and
// https://github.com/getsentry/sentry-javascript/issues/2380
sentryTest(
'should capture PromiseRejectionEvent cast to CustomEvent with type unhandledrejection',
async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData.exception?.values).toHaveLength(1);
expect(eventData.exception?.values?.[0]).toMatchObject({
type: 'Error',
value: 'promiseError',
mechanism: {
type: 'onunhandledrejection',
handled: false,
},
stacktrace: {
frames: expect.any(Array),
},
});
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function run() {
window.dispatchEvent(new Event('unhandledrejection'));
}

run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';

import { sentryTest } from '../../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';

// there's no evidence that this actually happens, but it could, and our code correctly
// handles it, so might as well prevent future regression on that score
sentryTest('should capture a random Event with type unhandledrejection', async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData.exception?.values).toHaveLength(1);
expect(eventData.exception?.values?.[0]).toMatchObject({
type: 'Event',
value: 'Event `Event` (type=unhandledrejection) captured as promise rejection',
mechanism: {
type: 'onunhandledrejection',
handled: false,
},
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://[email protected]/1337',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function run() {
const reason = new Error('promiseError');
const promise = Promise.reject(reason);
const event = new PromiseRejectionEvent('unhandledrejection', { promise, reason });
// simulate window.onunhandledrejection without generating a Script error
window.onunhandledrejection(event);
}

run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';

import { sentryTest } from '../../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';

sentryTest('should catch thrown errors', async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData.exception?.values).toHaveLength(1);
expect(eventData.exception?.values?.[0]).toMatchObject({
type: 'Error',
value: 'promiseError',
mechanism: {
type: 'onunhandledrejection',
handled: false,
},
stacktrace: {
frames: expect.any(Array),
},
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function run() {
const reason = null;
const promise = Promise.reject(reason);
const event = new PromiseRejectionEvent('unhandledrejection', { promise, reason });
// simulate window.onunhandledrejection without generating a Script error
window.onunhandledrejection(event);
}

run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';

import { sentryTest } from '../../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';

sentryTest('should catch thrown strings', async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData.exception?.values).toHaveLength(1);
expect(eventData.exception?.values?.[0]).toMatchObject({
type: 'UnhandledRejection',
value: 'Non-Error promise rejection captured with value: null',
mechanism: {
type: 'onunhandledrejection',
handled: false,
},
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function run() {
const reason = 123;
const promise = Promise.reject(reason);
const event = new PromiseRejectionEvent('unhandledrejection', { promise, reason });
// simulate window.onunhandledrejection without generating a Script error
window.onunhandledrejection(event);
}

run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';

import { sentryTest } from '../../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';

sentryTest('should catch thrown strings', async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData.exception?.values).toHaveLength(1);
expect(eventData.exception?.values?.[0]).toMatchObject({
type: 'UnhandledRejection',
value: 'Non-Error promise rejection captured with value: 123',
mechanism: {
type: 'onunhandledrejection',
handled: false,
},
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
function run() {
const reason = {
a: '1'.repeat('100'),
b: '2'.repeat('100'),
c: '3'.repeat('100'),
};
reason.d = reason.a;
reason.e = reason;
const promise = Promise.reject(reason);
const event = new PromiseRejectionEvent('unhandledrejection', { promise, reason });
// simulate window.onunhandledrejection without generating a Script error
window.onunhandledrejection(event);
}

run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';

import { sentryTest } from '../../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';

sentryTest('should capture unhandledrejection with a complex object', async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData.exception?.values).toHaveLength(1);
expect(eventData.exception?.values?.[0]).toMatchObject({
type: 'UnhandledRejection',
value: 'Object captured as promise rejection with keys: a, b, c, d, e',
mechanism: {
type: 'onunhandledrejection',
handled: false,
},
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function run() {
const reason = { a: 'b', b: 'c', c: 'd' };
const promise = Promise.reject(reason);
const event = new PromiseRejectionEvent('unhandledrejection', { promise, reason });
// simulate window.onunhandledrejection without generating a Script error
window.onunhandledrejection(event);
}

run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';

import { sentryTest } from '../../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';

sentryTest('should capture unhandledrejection with an object', async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData.exception?.values).toHaveLength(1);
expect(eventData.exception?.values?.[0]).toMatchObject({
type: 'UnhandledRejection',
value: 'Object captured as promise rejection with keys: a, b, c',
mechanism: {
type: 'onunhandledrejection',
handled: false,
},
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function run() {
const reason = 'stringError'.repeat(100);
const promise = Promise.reject(reason);
const event = new PromiseRejectionEvent('unhandledrejection', { promise, reason });
// simulate window.onunhandledrejection without generating a Script error
window.onunhandledrejection(event);
}

run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';

import { sentryTest } from '../../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';

sentryTest('should capture unhandledrejection with a large string', async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData.exception?.values).toHaveLength(1);
expect(eventData.exception?.values?.[0]).toMatchObject({
type: 'UnhandledRejection',
value:
'Non-Error promise rejection captured with value: stringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstr...',
mechanism: {
type: 'onunhandledrejection',
handled: false,
},
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function run() {
const reason = 'stringError';
const promise = Promise.reject(reason);
const event = new PromiseRejectionEvent('unhandledrejection', { promise, reason });
// simulate window.onunhandledrejection without generating a Script error
window.onunhandledrejection(event);
}

run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';

import { sentryTest } from '../../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';

sentryTest('should catch thrown strings', async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });

const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData.exception?.values).toHaveLength(1);
expect(eventData.exception?.values?.[0]).toMatchObject({
type: 'UnhandledRejection',
value: 'Non-Error promise rejection captured with value: stringError',
mechanism: {
type: 'onunhandledrejection',
handled: false,
},
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function run() {
const reason = undefined;
const promise = Promise.reject(reason);
const event = new PromiseRejectionEvent('unhandledrejection', { promise, reason });
// simulate window.onunhandledrejection without generating a Script error
window.onunhandledrejection(event);
}

run();
Loading

0 comments on commit 3eea537

Please sign in to comment.