Skip to content

Commit b61d820

Browse files
AbhiPrasadmydea
andauthored
test(browser): Port builtin tests to playwright (#11733)
This ports `packages/browser/test/integration/suites/builtins.js` to playwright. It also re-organizes the `eventListener` test structure to be a little more organized. The new test suites added are instrumentation for `xhr` and `requestAnimationFrame` Co-authored-by: Francesco Novy <[email protected]>
1 parent 0a440dd commit b61d820

File tree

30 files changed

+315
-332
lines changed

30 files changed

+315
-332
lines changed
Lines changed: 7 additions & 0 deletions
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+
});
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from '@playwright/test';
22

3-
import { sentryTest } from '../../../../utils/fixtures';
3+
import { sentryTest } from '../../../../../utils/fixtures';
44

55
sentryTest(
66
'Event listener instrumentation should attach the same event listener only once',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
function clickHandler() {
2+
throw new Error('event_listener_error');
3+
}
4+
5+
window.addEventListener('click', clickHandler);
6+
7+
document.body.click();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 built-in handlers fn name in mechanism data', 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: 'event_listener_error',
16+
mechanism: {
17+
type: 'instrument',
18+
handled: false,
19+
data: {
20+
function: 'addEventListener',
21+
handler: 'clickHandler',
22+
target: 'EventTarget',
23+
},
24+
},
25+
stacktrace: {
26+
frames: expect.any(Array),
27+
},
28+
});
29+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// store references to original, unwrapped built-ins in order to make assertions re: wrapped functions
2+
window.originalBuiltIns = {
3+
addEventListener: document.addEventListener,
4+
};
5+
6+
import * as Sentry from '@sentry/browser';
7+
8+
window.Sentry = Sentry;
9+
10+
Sentry.init({
11+
dsn: 'https://[email protected]/1337',
12+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const div = document.createElement('div');
2+
document.body.appendChild(div);
3+
window.capturedCall = false;
4+
const captureFn = function () {
5+
window.capturedCall = true;
6+
};
7+
// Use original addEventListener to simulate non-wrapped behavior (callback is attached without __sentry_wrapped__)
8+
window.originalBuiltIns.addEventListener.call(div, 'click', captureFn);
9+
// Then attach the same callback again, but with already wrapped method
10+
div.addEventListener('click', captureFn);
11+
div.removeEventListener('click', captureFn);
12+
div.dispatchEvent(new MouseEvent('click'));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../../../utils/fixtures';
4+
5+
sentryTest(
6+
'should remove the original callback if it was registered before Sentry initialized (w. original method)',
7+
async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
await page.goto(url);
11+
12+
const capturedCalled = await page.evaluate(() => {
13+
// @ts-expect-error defined in subject.js
14+
return window.capturedCall;
15+
});
16+
17+
expect(capturedCalled).toBe(false);
18+
},
19+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const div = document.createElement('div');
2+
document.body.appendChild(div);
3+
const fooFn = function () {
4+
throw new Error('foo');
5+
};
6+
const barFn = function () {
7+
throw new Error('bar');
8+
};
9+
div.addEventListener('click', fooFn);
10+
div.addEventListener('click', barFn);
11+
div.removeEventListener('click', barFn);
12+
div.dispatchEvent(new MouseEvent('click'));
Lines changed: 24 additions & 0 deletions
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 transparently remove event listeners from wrapped functions', 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: 'foo',
16+
mechanism: {
17+
type: 'instrument',
18+
handled: false,
19+
},
20+
stacktrace: {
21+
frames: expect.any(Array),
22+
},
23+
});
24+
});
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from '@playwright/test';
22

3-
import { sentryTest } from '../../../../utils/fixtures';
3+
import { sentryTest } from '../../../../../utils/fixtures';
44

55
sentryTest('Event listener instrumentation preserves "this" context', async ({ getLocalTestPath, page }) => {
66
const url = await getLocalTestPath({ testDir: __dirname });

dev-packages/browser-integration-tests/suites/public-api/instrumentation/eventListener/test.ts renamed to dev-packages/browser-integration-tests/suites/public-api/instrumentation/eventListener/thrown-error/test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { expect } from '@playwright/test';
22
import type { Event } from '@sentry/types';
33

4-
import { sentryTest } from '../../../../utils/fixtures';
5-
import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers';
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers';
66

77
sentryTest(
88
'Event listener instrumentation should capture an error thrown in an event handler',
@@ -18,6 +18,11 @@ sentryTest(
1818
mechanism: {
1919
type: 'instrument',
2020
handled: false,
21+
data: {
22+
function: 'addEventListener',
23+
handler: '<anonymous>',
24+
target: 'EventTarget',
25+
},
2126
},
2227
stacktrace: {
2328
frames: expect.any(Array),
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from '@playwright/test';
22

3-
import { sentryTest } from '../../../../utils/fixtures';
3+
import { sentryTest } from '../../../../../utils/fixtures';
44

55
sentryTest(
66
'Event listener instrumentation should not wrap event listeners multiple times',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../../../utils/fixtures';
4+
5+
sentryTest(
6+
'wrapped callback should preserve correct context - window (not-bound)',
7+
async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
await page.goto(url);
11+
12+
const { outsideCtx, requestAnimationFrameCtx } = (await page.evaluate(() => {
13+
return new Promise(resolve => {
14+
const outsideCtx = window as any;
15+
requestAnimationFrame(function () {
16+
// @ts-expect-error re-assigning this
17+
resolve({ outsideCtx, requestAnimationFrameCtx: this as any });
18+
});
19+
});
20+
})) as any;
21+
expect(requestAnimationFrameCtx).toBe(outsideCtx);
22+
},
23+
);
24+
25+
sentryTest(
26+
'wrapped callback should preserve correct context - `bind` bound method',
27+
async ({ getLocalTestPath, page }) => {
28+
const url = await getLocalTestPath({ testDir: __dirname });
29+
30+
await page.goto(url);
31+
32+
const requestAnimationFrameCtx = (await page.evaluate(() => {
33+
return new Promise(resolve => {
34+
function foo() {
35+
// @ts-expect-error re-assigning this
36+
resolve(this);
37+
}
38+
39+
requestAnimationFrame(foo.bind({ magicNumber: 42 }));
40+
});
41+
})) as any;
42+
43+
expect(requestAnimationFrameCtx.magicNumber).toBe(42);
44+
},
45+
);
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,3 @@
1+
requestAnimationFrame(function () {
2+
throw new Error('requestAnimationFrame_error');
3+
});
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 capture exceptions inside callback', 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: 'requestAnimationFrame_error',
16+
mechanism: {
17+
type: 'instrument',
18+
handled: false,
19+
},
20+
stacktrace: {
21+
frames: expect.any(Array),
22+
},
23+
});
24+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
let exceptionInterval = setInterval(function () {
2+
clearInterval(exceptionInterval);
3+
throw new Error('setInterval_error');
4+
}, 0);
Lines changed: 24 additions & 0 deletions
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('Instrumentation should capture errors in setInterval', 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: 'setInterval_error',
16+
mechanism: {
17+
type: 'instrument',
18+
handled: false,
19+
},
20+
stacktrace: {
21+
frames: expect.any(Array),
22+
},
23+
});
24+
});

dev-packages/browser-integration-tests/suites/public-api/instrumentation/setTimeout/test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ sentryTest('Instrumentation should capture errors in setTimeout', async ({ getLo
1616
mechanism: {
1717
type: 'instrument',
1818
handled: false,
19+
data: {
20+
function: 'setTimeout',
21+
},
1922
},
2023
stacktrace: {
2124
frames: expect.any(Array),
Lines changed: 7 additions & 0 deletions
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,7 @@
1+
window.calls = {};
2+
const xhr = new XMLHttpRequest();
3+
xhr.open('GET', 'test');
4+
xhr.onreadystatechange = function wat() {
5+
window.calls[xhr.readyState] = window.calls[xhr.readyState] ? window.calls[xhr.readyState] + 1 : 1;
6+
};
7+
xhr.send();
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../../../utils/fixtures';
4+
5+
sentryTest(
6+
'should not call XMLHttpRequest onreadystatechange more than once per state',
7+
async ({ getLocalTestPath, page }) => {
8+
const url = await getLocalTestPath({ testDir: __dirname });
9+
10+
await page.goto(url);
11+
12+
const calls = await page.evaluate(() => {
13+
// @ts-expect-error window.calls defined in subject.js
14+
return window.calls;
15+
});
16+
17+
expect(calls).toEqual({ '4': 1 });
18+
},
19+
);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const xhr = new XMLHttpRequest();
2+
xhr.open('GET', 'test');
3+
// intentionally assign event handlers *after* open, since this is what jQuery does
4+
xhr.onreadystatechange = function wat() {
5+
// replace onreadystatechange with no-op so exception doesn't
6+
// fire more than once as XHR changes loading state
7+
xhr.onreadystatechange = function () {};
8+
throw new Error('xhr_error');
9+
};
10+
xhr.send();
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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(
8+
'should capture exceptions from XMLHttpRequest event handlers (e.g. onreadystatechange)',
9+
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: 'Error',
17+
value: 'xhr_error',
18+
mechanism: {
19+
type: 'instrument',
20+
handled: false,
21+
data: {
22+
function: 'onreadystatechange',
23+
},
24+
},
25+
stacktrace: {
26+
frames: expect.any(Array),
27+
},
28+
});
29+
},
30+
);

0 commit comments

Comments
 (0)