Skip to content

Commit 3eea537

Browse files
authored
test(browser): Port unhandledrejection tests to playwright (#11758)
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.
1 parent de98428 commit 3eea537

File tree

50 files changed

+458
-4892
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+458
-4892
lines changed

.github/workflows/build.yml

-35
Original file line numberDiff line numberDiff line change
@@ -768,40 +768,6 @@ jobs:
768768
name: playwright-traces
769769
path: dev-packages/browser-integration-tests/test-results
770770

771-
job_browser_integration_tests:
772-
name: Browser (${{ matrix.browser }}) Tests
773-
needs: [job_get_metadata, job_build]
774-
if: needs.job_get_metadata.outputs.changed_browser == 'true' || github.event_name != 'pull_request'
775-
runs-on: ubuntu-20.04
776-
timeout-minutes: 20
777-
strategy:
778-
fail-fast: false
779-
matrix:
780-
browser:
781-
- ChromeHeadless
782-
- FirefoxHeadless
783-
- WebkitHeadless
784-
steps:
785-
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
786-
uses: actions/checkout@v4
787-
with:
788-
ref: ${{ env.HEAD_COMMIT }}
789-
- name: Set up Node
790-
uses: actions/setup-node@v4
791-
with:
792-
node-version-file: 'package.json'
793-
- name: Restore caches
794-
uses: ./.github/actions/restore-cache
795-
env:
796-
DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }}
797-
- name: Run integration tests
798-
env:
799-
KARMA_BROWSER: ${{ matrix.browser }}
800-
run: |
801-
cd packages/browser
802-
[[ $KARMA_BROWSER == WebkitHeadless ]] && yarn run playwright install-deps webkit
803-
yarn test:integration
804-
805771
job_browser_build_tests:
806772
name: Browser Build Tests
807773
needs: [job_get_metadata, job_build]
@@ -1236,7 +1202,6 @@ jobs:
12361202
job_nextjs_integration_test,
12371203
job_node_integration_tests,
12381204
job_browser_playwright_tests,
1239-
job_browser_integration_tests,
12401205
job_browser_loader_tests,
12411206
job_remix_integration_tests,
12421207
job_e2e_tests,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
function run() {
2+
// this isn't how it happens in real life, in that the promise and reason
3+
// values come from an actual PromiseRejectionEvent, but it's enough to test
4+
// how the SDK handles the structure
5+
window.dispatchEvent(
6+
new CustomEvent('unhandledrejection', {
7+
detail: {
8+
promise: new Promise(function () {}),
9+
// we're testing with an error here but it could be anything - really
10+
// all we're testing is that it gets dug out correctly
11+
reason: new Error('promiseError'),
12+
},
13+
}),
14+
);
15+
}
16+
17+
run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';
6+
7+
// something, somewhere, (likely a browser extension) effectively casts PromiseRejectionEvents
8+
// to CustomEvents, moving the `promise` and `reason` attributes of the PRE into
9+
// the CustomEvent's `detail` attribute, since they're not part of CustomEvent's spec
10+
// see https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent and
11+
// https://github.com/getsentry/sentry-javascript/issues/2380
12+
sentryTest(
13+
'should capture PromiseRejectionEvent cast to CustomEvent with type unhandledrejection',
14+
async ({ getLocalTestPath, page }) => {
15+
const url = await getLocalTestPath({ testDir: __dirname });
16+
17+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
18+
19+
expect(eventData.exception?.values).toHaveLength(1);
20+
expect(eventData.exception?.values?.[0]).toMatchObject({
21+
type: 'Error',
22+
value: 'promiseError',
23+
mechanism: {
24+
type: 'onunhandledrejection',
25+
handled: false,
26+
},
27+
stacktrace: {
28+
frames: expect.any(Array),
29+
},
30+
});
31+
},
32+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
function run() {
2+
window.dispatchEvent(new Event('unhandledrejection'));
3+
}
4+
5+
run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';
6+
7+
// there's no evidence that this actually happens, but it could, and our code correctly
8+
// handles it, so might as well prevent future regression on that score
9+
sentryTest('should capture a random Event with type unhandledrejection', async ({ getLocalTestPath, page }) => {
10+
const url = await getLocalTestPath({ testDir: __dirname });
11+
12+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
13+
14+
expect(eventData.exception?.values).toHaveLength(1);
15+
expect(eventData.exception?.values?.[0]).toMatchObject({
16+
type: 'Event',
17+
value: 'Event `Event` (type=unhandledrejection) captured as promise rejection',
18+
mechanism: {
19+
type: 'onunhandledrejection',
20+
handled: false,
21+
},
22+
});
23+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
function run() {
2+
const reason = new Error('promiseError');
3+
const promise = Promise.reject(reason);
4+
const event = new PromiseRejectionEvent('unhandledrejection', { promise, reason });
5+
// simulate window.onunhandledrejection without generating a Script error
6+
window.onunhandledrejection(event);
7+
}
8+
9+
run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';
6+
7+
sentryTest('should catch thrown errors', async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
11+
12+
expect(eventData.exception?.values).toHaveLength(1);
13+
expect(eventData.exception?.values?.[0]).toMatchObject({
14+
type: 'Error',
15+
value: 'promiseError',
16+
mechanism: {
17+
type: 'onunhandledrejection',
18+
handled: false,
19+
},
20+
stacktrace: {
21+
frames: expect.any(Array),
22+
},
23+
});
24+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
function run() {
2+
const reason = null;
3+
const promise = Promise.reject(reason);
4+
const event = new PromiseRejectionEvent('unhandledrejection', { promise, reason });
5+
// simulate window.onunhandledrejection without generating a Script error
6+
window.onunhandledrejection(event);
7+
}
8+
9+
run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';
6+
7+
sentryTest('should catch thrown strings', async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
11+
12+
expect(eventData.exception?.values).toHaveLength(1);
13+
expect(eventData.exception?.values?.[0]).toMatchObject({
14+
type: 'UnhandledRejection',
15+
value: 'Non-Error promise rejection captured with value: null',
16+
mechanism: {
17+
type: 'onunhandledrejection',
18+
handled: false,
19+
},
20+
});
21+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
function run() {
2+
const reason = 123;
3+
const promise = Promise.reject(reason);
4+
const event = new PromiseRejectionEvent('unhandledrejection', { promise, reason });
5+
// simulate window.onunhandledrejection without generating a Script error
6+
window.onunhandledrejection(event);
7+
}
8+
9+
run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';
6+
7+
sentryTest('should catch thrown strings', async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
11+
12+
expect(eventData.exception?.values).toHaveLength(1);
13+
expect(eventData.exception?.values?.[0]).toMatchObject({
14+
type: 'UnhandledRejection',
15+
value: 'Non-Error promise rejection captured with value: 123',
16+
mechanism: {
17+
type: 'onunhandledrejection',
18+
handled: false,
19+
},
20+
});
21+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
function run() {
2+
const reason = {
3+
a: '1'.repeat('100'),
4+
b: '2'.repeat('100'),
5+
c: '3'.repeat('100'),
6+
};
7+
reason.d = reason.a;
8+
reason.e = reason;
9+
const promise = Promise.reject(reason);
10+
const event = new PromiseRejectionEvent('unhandledrejection', { promise, reason });
11+
// simulate window.onunhandledrejection without generating a Script error
12+
window.onunhandledrejection(event);
13+
}
14+
15+
run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';
6+
7+
sentryTest('should capture unhandledrejection with a complex object', async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
11+
12+
expect(eventData.exception?.values).toHaveLength(1);
13+
expect(eventData.exception?.values?.[0]).toMatchObject({
14+
type: 'UnhandledRejection',
15+
value: 'Object captured as promise rejection with keys: a, b, c, d, e',
16+
mechanism: {
17+
type: 'onunhandledrejection',
18+
handled: false,
19+
},
20+
});
21+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
function run() {
2+
const reason = { a: 'b', b: 'c', c: 'd' };
3+
const promise = Promise.reject(reason);
4+
const event = new PromiseRejectionEvent('unhandledrejection', { promise, reason });
5+
// simulate window.onunhandledrejection without generating a Script error
6+
window.onunhandledrejection(event);
7+
}
8+
9+
run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';
6+
7+
sentryTest('should capture unhandledrejection with an object', async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
11+
12+
expect(eventData.exception?.values).toHaveLength(1);
13+
expect(eventData.exception?.values?.[0]).toMatchObject({
14+
type: 'UnhandledRejection',
15+
value: 'Object captured as promise rejection with keys: a, b, c',
16+
mechanism: {
17+
type: 'onunhandledrejection',
18+
handled: false,
19+
},
20+
});
21+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
function run() {
2+
const reason = 'stringError'.repeat(100);
3+
const promise = Promise.reject(reason);
4+
const event = new PromiseRejectionEvent('unhandledrejection', { promise, reason });
5+
// simulate window.onunhandledrejection without generating a Script error
6+
window.onunhandledrejection(event);
7+
}
8+
9+
run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';
6+
7+
sentryTest('should capture unhandledrejection with a large string', async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
11+
12+
expect(eventData.exception?.values).toHaveLength(1);
13+
expect(eventData.exception?.values?.[0]).toMatchObject({
14+
type: 'UnhandledRejection',
15+
value:
16+
'Non-Error promise rejection captured with value: stringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstringErrorstr...',
17+
mechanism: {
18+
type: 'onunhandledrejection',
19+
handled: false,
20+
},
21+
});
22+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
function run() {
2+
const reason = 'stringError';
3+
const promise = Promise.reject(reason);
4+
const event = new PromiseRejectionEvent('unhandledrejection', { promise, reason });
5+
// simulate window.onunhandledrejection without generating a Script error
6+
window.onunhandledrejection(event);
7+
}
8+
9+
run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';
6+
7+
sentryTest('should catch thrown strings', async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
11+
12+
expect(eventData.exception?.values).toHaveLength(1);
13+
expect(eventData.exception?.values?.[0]).toMatchObject({
14+
type: 'UnhandledRejection',
15+
value: 'Non-Error promise rejection captured with value: stringError',
16+
mechanism: {
17+
type: 'onunhandledrejection',
18+
handled: false,
19+
},
20+
});
21+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
function run() {
2+
const reason = undefined;
3+
const promise = Promise.reject(reason);
4+
const event = new PromiseRejectionEvent('unhandledrejection', { promise, reason });
5+
// simulate window.onunhandledrejection without generating a Script error
6+
window.onunhandledrejection(event);
7+
}
8+
9+
run();

0 commit comments

Comments
 (0)