Skip to content

Commit b2f2332

Browse files
committed
Add builtins tests.
1 parent 91dd30f commit b2f2332

File tree

1 file changed

+322
-0
lines changed

1 file changed

+322
-0
lines changed
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
const { test, expect } = require('@playwright/test');
2+
const Sentry = require('@sentry/browser');
3+
4+
const { getSentryEvents, getSentryRequest } = require('./utils/helpers');
5+
6+
test.describe('wrapped built-ins', () => {
7+
test.beforeEach(async ({ baseURL, page }) => {
8+
await page.goto(baseURL);
9+
});
10+
11+
test('should capture exceptions from event listeners', async ({ page }) => {
12+
const eventData = await getSentryEvents(page, () => {
13+
var div = document.createElement('div');
14+
document.body.appendChild(div);
15+
div.addEventListener(
16+
'click',
17+
function() {
18+
window.element = div;
19+
window.context = this;
20+
// eslint-disable-next-line no-undef
21+
foo();
22+
},
23+
false,
24+
);
25+
26+
var click = new MouseEvent('click');
27+
div.dispatchEvent(click);
28+
});
29+
30+
const element = await page.evaluate(() => window.element);
31+
const context = await page.evaluate(() => window.context);
32+
expect(element).toMatchObject(context);
33+
expect(eventData[0].exception.values[0].value).toMatch(/foo/);
34+
});
35+
36+
test('should transparently remove event listeners from wrapped functions', async ({ page }) => {
37+
const eventData = await getSentryEvents(page, () => {
38+
var div = document.createElement('div');
39+
document.body.appendChild(div);
40+
var fooFn = function() {
41+
// eslint-disable-next-line no-undef
42+
foo();
43+
};
44+
var barFn = function() {
45+
// eslint-disable-next-line no-undef
46+
bar();
47+
};
48+
div.addEventListener('click', fooFn);
49+
div.addEventListener('click', barFn);
50+
div.removeEventListener('click', barFn);
51+
div.dispatchEvent(new MouseEvent('click'));
52+
});
53+
54+
expect(eventData).toHaveLength(1);
55+
});
56+
57+
test('should remove the original callback if it was registered before Sentry initialized (w. original method)', async ({
58+
page,
59+
}) => {
60+
await getSentryEvents(page, () => {
61+
var div = document.createElement('div');
62+
document.body.appendChild(div);
63+
window.capturedCall = false;
64+
var captureFn = function() {
65+
window.capturedCall = true;
66+
};
67+
// Use original addEventListener to simulate non-wrapped behavior (callback is attached without __sentry_wrapped__)
68+
window.originalBuiltIns.addEventListener.call(div, 'click', captureFn);
69+
// Then attach the same callback again, but with already wrapped method
70+
div.addEventListener('click', captureFn);
71+
div.removeEventListener('click', captureFn);
72+
div.dispatchEvent(new MouseEvent('click'));
73+
});
74+
75+
const capturedCall = await page.evaluate(() => window.capturedCall);
76+
77+
expect(capturedCall).toBe(false);
78+
});
79+
80+
test('should capture exceptions inside setTimeout', async ({ page }) => {
81+
const eventData = await getSentryEvents(page, () => {
82+
setTimeout(function() {
83+
// eslint-disable-next-line no-undef
84+
foo();
85+
});
86+
});
87+
88+
expect(eventData[0].exception.values[0].value).toMatch(/foo/);
89+
});
90+
91+
test('should capture exceptions inside setInterval', async ({ page }) => {
92+
const eventData = await getSentryEvents(page, () => {
93+
var exceptionInterval = setInterval(function() {
94+
clearInterval(exceptionInterval);
95+
// eslint-disable-next-line no-undef
96+
foo();
97+
}, 0);
98+
});
99+
100+
expect(eventData[0].exception.values[0].value).toMatch(/foo/);
101+
});
102+
103+
test.describe('requestAnimationFrame', () => {
104+
test('should capture exceptions inside callback', async ({ page }) => {
105+
// wait for page to be visible or requestAnimationFrame won't ever fire
106+
await page.waitForSelector('body');
107+
108+
// Note: Using `getSentryRequest` here as it waits for the callback inside `requestAnimationFrame` by default.
109+
const requestData = await getSentryRequest(page, () => {
110+
requestAnimationFrame(function() {
111+
// eslint-disable-next-line no-undef
112+
foo();
113+
});
114+
});
115+
116+
expect(requestData.exception.values[0].value).toMatch(/foo/);
117+
});
118+
119+
test('wrapped callback should preserve correct context - window (not-bound)', async ({ page }) => {
120+
// wait for page to be visible or requestAnimationFrame won't ever fire
121+
await page.waitForSelector('body');
122+
123+
// Note: Using `getSentryRequest` here as it waits for the callback inside `requestAnimationFrame` by default.
124+
await getSentryRequest(page, () => {
125+
// Note: testing whole `window` object does not work as `window` object is not guaranteed to be serializable.
126+
// Playwright returns `undefined` from `evaluate` if the return value is non-serializable.
127+
window.fooBar = { foo: 'bar' };
128+
requestAnimationFrame(function() {
129+
window.capturedCtx = this;
130+
131+
// Capturing a message to trigger `getSentryRequest`.
132+
Sentry.captureMessage('foo');
133+
});
134+
});
135+
136+
const capturedCtx = await page.evaluate(() => window.capturedCtx.fooBar);
137+
const window = await page.evaluate(() => window.fooBar);
138+
139+
expect(capturedCtx).toMatchObject(window);
140+
});
141+
142+
test('wrapped callback should preserve correct context - class bound method', async ({ page }) => {
143+
// wait for page to be visible or requestAnimationFrame won't ever fire
144+
await page.waitForSelector('body');
145+
146+
// Note: Using `getSentryRequest` here as it waits for the callback inside `requestAnimationFrame` by default.
147+
await getSentryRequest(page, () => {
148+
// TypeScript-transpiled class syntax
149+
var Foo = (function() {
150+
function Foo() {
151+
var _this = this;
152+
this.magicNumber = 42;
153+
this.getThis = function() {
154+
window.capturedCtx = _this;
155+
// Capturing a message to trigger `getSentryRequest`.
156+
Sentry.captureMessage('foo');
157+
};
158+
}
159+
return Foo;
160+
})();
161+
var foo = new Foo();
162+
requestAnimationFrame(foo.getThis);
163+
});
164+
165+
const capturedCtx = await page.evaluate(() => window.capturedCtx);
166+
167+
expect(capturedCtx.magicNumber).toBe(42);
168+
});
169+
170+
test('wrapped callback should preserve correct context - `bind` bound method', async ({ page }) => {
171+
// wait for page to be visible or requestAnimationFrame won't ever fire
172+
await page.waitForSelector('body');
173+
174+
// Note: Using `getSentryRequest` here as it waits for the callback inside `requestAnimationFrame` by default.
175+
await getSentryRequest(page, () => {
176+
function foo() {
177+
window.capturedCtx = this;
178+
// Capturing a message to trigger `getSentryRequest`.
179+
Sentry.captureMessage('foo');
180+
}
181+
requestAnimationFrame(foo.bind({ magicNumber: 42 }));
182+
});
183+
184+
const capturedCtx = await page.evaluate(() => window.capturedCtx);
185+
186+
expect(capturedCtx.magicNumber).toBe(42);
187+
});
188+
});
189+
190+
test('should capture exceptions from XMLHttpRequest event handlers (e.g. onreadystatechange)', async ({ page }) => {
191+
// Note: Using `getSentryRequest` here as it waits for the callback inside `requestAnimationFrame` by default.
192+
const requestData = await getSentryRequest(page, () => {
193+
var xhr = new XMLHttpRequest();
194+
xhr.open('GET', '/base/subjects/example.json');
195+
// intentionally assign event handlers *after* open, since this is what jQuery does
196+
xhr.onreadystatechange = function wat() {
197+
// replace onreadystatechange with no-op so exception doesn't
198+
// fire more than once as XHR changes loading state
199+
xhr.onreadystatechange = function() {};
200+
// eslint-disable-next-line no-undef
201+
foo();
202+
// Capturing a message to trigger `getSentryRequest`.
203+
Sentry.captureMessage('foo');
204+
};
205+
xhr.send();
206+
});
207+
208+
expect(requestData.exception.values[0].value).toMatch(/foo/);
209+
expect(requestData.exception.values[0].mechanism).toMatchObject({
210+
type: 'instrument',
211+
handled: true,
212+
data: {
213+
function: 'onreadystatechange',
214+
},
215+
});
216+
});
217+
218+
test('should not call XMLHttpRequest onreadystatechange more than once per state', async ({ page }) => {
219+
// Note: Using `getSentryRequest` here as it waits for the callback inside `requestAnimationFrame` by default.
220+
await getSentryRequest(page, () => {
221+
window.calls = {};
222+
var xhr = new XMLHttpRequest();
223+
xhr.open('GET', '/base/subjects/example.json');
224+
xhr.onreadystatechange = function wat() {
225+
window.calls[xhr.readyState] = window.calls[xhr.readyState] ? window.calls[xhr.readyState] + 1 : 1;
226+
if (xhr.readyState === 4) {
227+
// Capturing a message to trigger `getSentryRequest`.
228+
Sentry.captureMessage('foo');
229+
}
230+
};
231+
xhr.send();
232+
});
233+
234+
const calls = await page.evaluate(() => window.calls);
235+
236+
// eslint-disable-next-line guard-for-in
237+
for (const state in calls) {
238+
expect(calls[state]).toBe(1);
239+
}
240+
241+
expect(Object.keys(calls).length).toBeGreaterThanOrEqual(3);
242+
expect(Object.keys(calls).length).toBeLessThanOrEqual(4);
243+
});
244+
245+
test(`should capture built-in's mechanism type as instrument`, async ({ page }) => {
246+
const requestData = await getSentryRequest(page, () => {
247+
setTimeout(function() {
248+
// eslint-disable-next-line no-undef
249+
foo();
250+
});
251+
});
252+
253+
const fn = requestData.exception.values[0].mechanism.data.function;
254+
255+
expect(fn).toBe('setTimeout');
256+
257+
expect(requestData.exception.values[0].mechanism).toMatchObject({
258+
type: 'instrument',
259+
handled: true,
260+
});
261+
});
262+
263+
test(`should capture built-in's handlers fn name in mechanism data`, async ({ page }) => {
264+
const requestData = await getSentryRequest(page, () => {
265+
var div = document.createElement('div');
266+
document.body.appendChild(div);
267+
div.addEventListener(
268+
'click',
269+
function namedFunction() {
270+
// eslint-disable-next-line no-undef
271+
foo();
272+
},
273+
false,
274+
);
275+
var click = new MouseEvent('click');
276+
div.dispatchEvent(click);
277+
});
278+
279+
const fn = requestData.exception.values[0].mechanism.data.function;
280+
expect(fn).toBe('addEventListener');
281+
282+
const handler = requestData.exception.values[0].mechanism.data.handler;
283+
expect(handler).toBe('namedFunction');
284+
285+
expect(requestData.exception.values[0].mechanism).toMatchObject({
286+
type: 'instrument',
287+
handled: true,
288+
data: {
289+
function: 'addEventListener',
290+
},
291+
});
292+
});
293+
294+
test(`should fallback to <anonymous> fn name in mechanism data if one is unavailable`, async ({ page }) => {
295+
const requestData = await getSentryRequest(page, () => {
296+
var div = document.createElement('div');
297+
document.body.appendChild(div);
298+
div.addEventListener(
299+
'click',
300+
function() {
301+
// eslint-disable-next-line no-undef
302+
foo();
303+
},
304+
false,
305+
);
306+
var click = new MouseEvent('click');
307+
div.dispatchEvent(click);
308+
});
309+
310+
const target = requestData.exception.values[0].mechanism.data.target;
311+
expect(target).toBe('EventTarget');
312+
313+
expect(requestData.exception.values[0].mechanism).toMatchObject({
314+
type: 'instrument',
315+
handled: true,
316+
data: {
317+
function: 'addEventListener',
318+
handler: '<anonymous>',
319+
},
320+
});
321+
});
322+
});

0 commit comments

Comments
 (0)