Skip to content

Commit 2bc1823

Browse files
authored
test(browser): Simple PoC for end-to-end browser tests using Playwright.
1 parent d3ca7a8 commit 2bc1823

File tree

12 files changed

+3281
-1
lines changed

12 files changed

+3281
-1
lines changed

packages/browser/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,13 @@
7272
"fix": "run-s fix:eslint fix:prettier",
7373
"fix:prettier": "prettier --write \"{src,test}/**/*.ts\"",
7474
"fix:eslint": "eslint . --format stylish --fix",
75-
"test": "run-s test:unit",
75+
"test": "run-s test:unit test:e2e",
7676
"test:unit": "karma start test/unit/karma.conf.js",
7777
"test:unit:watch": "karma start test/unit/karma.conf.js --auto-watch --no-single-run",
7878
"test:integration": "test/integration/run.js",
7979
"test:integration:watch": "test/integration/run.js --watch",
8080
"test:integration:checkbrowsers": "node scripts/checkbrowsers.js",
81+
"test:e2e": "yarn --cwd ./test/e2e run test",
8182
"test:package": "node test/package/npm-build.js && rm test/package/tmp.js",
8283
"size:check": "run-p size:check:es5 size:check:es6",
8384
"size:check:es5": "cat build/bundle.min.js | gzip -9 | wc -c | awk '{$1=$1/1024; print \"ES5: \",$1,\"kB\";}'",

packages/browser/test/e2e/.eslintrc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"ecmaFeatures": {
3+
"modules": true,
4+
"spread": true,
5+
"restParams": true
6+
},
7+
"env": {
8+
"browser": true,
9+
"node": true,
10+
"es6": true
11+
},
12+
"rules": {
13+
"no-unused-vars": 2,
14+
"no-undef": 2
15+
},
16+
"parserOptions": {
17+
"sourceType": "module"
18+
}
19+
}

packages/browser/test/e2e/index.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!DOCTYPE html>
2+
<html lang="en" dir="ltr">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
7+
<title>Sample Webpack App</title>
8+
</head>
9+
<body>
10+
<!-- For Breadcrumbs Tests -->
11+
<form id="foo-form">
12+
<input name="foo" placeholder="lol" />
13+
<div class="contenteditable" contenteditable="true"></div>
14+
</form>
15+
<script src="bundle.js"></script>
16+
</body>
17+
</html>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "sentry-browser-integration-test-app",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"private": true,
7+
"babel": {
8+
"presets": [
9+
"@babel/preset-env"
10+
]
11+
},
12+
"scripts": {
13+
"postinstall": "playwright install --with-deps",
14+
"pretest": "yarn install && webpack",
15+
"test": "playwright test ./test --browser=all",
16+
"posttest": "rimraf dist",
17+
"dev": "webpack-dev-server"
18+
},
19+
"dependencies": {
20+
"@babel/core": "^7.15.5",
21+
"@babel/preset-env": "^7.15.4",
22+
"@playwright/test": "^1.14.1",
23+
"@sentry/browser": "file:../..",
24+
"babel-loader": "^8.2.2",
25+
"express": "^4.17.1",
26+
"webpack": "^5.52.0",
27+
"webpack-cli": "^4.8.0"
28+
},
29+
"resolutions": {
30+
"@sentry/core": "file:../../../core",
31+
"@sentry/hub": "file:../../../hub",
32+
"@sentry/integrations": "file:../../../integrations",
33+
"@sentry/minimal": "file:../../../minimal",
34+
"@sentry/tracing": "file:../../../tracing",
35+
"@sentry/types": "file:../../../types",
36+
"@sentry/utils": "file:../../../utils"
37+
}
38+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const config = {
2+
// Retries for cases that might be flaky
3+
// Even if the test passes the second time, it's reported as flaky
4+
// But the suite is considered successful.
5+
retries: 2,
6+
webServer: {
7+
command: 'node server.js',
8+
port: 8080,
9+
timeout: 120 * 1000,
10+
},
11+
};
12+
13+
module.exports = config;

packages/browser/test/e2e/server.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const path = require('path');
2+
3+
const express = require('express');
4+
5+
const app = express();
6+
const port = process.env.PORT || 8080;
7+
8+
app.use(express.static(path.resolve(__dirname, './dist')));
9+
10+
app.get('/', function(_req, res) {
11+
res.sendFile(path.join(__dirname, '/index.html'));
12+
});
13+
14+
app.listen(port);
15+
console.log('Server started at http://localhost:' + port);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const Sentry = require('@sentry/browser');
2+
3+
Sentry.init({
4+
dsn: 'https://[email protected]/1337',
5+
});
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
const { test, expect } = require('@playwright/test');
2+
const Sentry = require('@sentry/browser');
3+
4+
const { waitForXHR, getSentryEvent } = require('./utils/helpers');
5+
6+
test.beforeEach(async ({ baseURL, page }) => {
7+
await page.goto(baseURL);
8+
});
9+
10+
test('records breadcrumbs for a simple XHR `GET` request', async ({ page }) => {
11+
const eventData = await getSentryEvent(
12+
page,
13+
() => {
14+
const xhr = new XMLHttpRequest();
15+
xhr.open('GET', '/base/subjects/example.json');
16+
xhr.onreadystatechange = function() {};
17+
xhr.send();
18+
19+
waitForXHR(xhr, function() {
20+
Sentry.captureMessage('test');
21+
});
22+
},
23+
{ waitForXHR },
24+
);
25+
26+
expect(eventData.breadcrumbs.length).toBe(1);
27+
expect(eventData.breadcrumbs[0].category).toBe('xhr');
28+
expect(eventData.breadcrumbs[0].type).toBe('http');
29+
expect(eventData.breadcrumbs[0].data.method).toBe('GET');
30+
expect(eventData.breadcrumbs[0].data.url).toBe('/base/subjects/example.json');
31+
});
32+
33+
test('records breadcrumbs for a fetch request', async ({ page }) => {
34+
const eventData = await getSentryEvent(page, () => {
35+
fetch('/base/subjects/example.json', {
36+
method: 'GET',
37+
}).then(function() {
38+
Sentry.captureMessage('test');
39+
});
40+
});
41+
42+
expect(eventData.breadcrumbs.length).toBe(1);
43+
expect(eventData.breadcrumbs[0].category).toBe('fetch');
44+
expect(eventData.breadcrumbs[0].type).toBe('http');
45+
expect(eventData.breadcrumbs[0].data.method).toBe('GET');
46+
expect(eventData.breadcrumbs[0].data.url).toBe('/base/subjects/example.json');
47+
});
48+
49+
test('records record a mouse click on element WITH click handler present', async ({ page }) => {
50+
const eventData = await getSentryEvent(page, () => {
51+
// add an event listener to the input. we want to make sure that
52+
// our breadcrumbs still work even if the page has an event listener
53+
// on an element that cancels event bubbling
54+
55+
var input = document.getElementsByTagName('input')[0];
56+
var clickHandler = function(evt) {
57+
evt.stopPropagation(); // don't bubble
58+
};
59+
input.addEventListener('click', clickHandler);
60+
61+
// click <input/>
62+
var click = new MouseEvent('click');
63+
input.dispatchEvent(click);
64+
65+
Sentry.captureMessage('test');
66+
});
67+
68+
expect(eventData.breadcrumbs.length).toBe(1);
69+
expect(eventData.breadcrumbs[0].category).toBe('ui.click');
70+
expect(eventData.breadcrumbs[0].message).toBe('body > form#foo-form > input[name="foo"]');
71+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const { test, expect } = require('@playwright/test');
2+
3+
const { getSentryEvent } = require('./utils/helpers');
4+
5+
test.beforeEach(async ({ baseURL, page }) => {
6+
await page.goto(baseURL);
7+
});
8+
9+
test('captures manually thrown error', async ({ page }) => {
10+
const eventData = await getSentryEvent(page, () => {
11+
throw new Error('Sentry Test');
12+
});
13+
14+
expect(eventData.exception.values[0].type).toBe('Error');
15+
expect(eventData.exception.values[0].value).toBe('Sentry Test');
16+
});
17+
18+
test('captures undefined function call', async ({ page, browserName }) => {
19+
const eventData = await getSentryEvent(page, () => {
20+
// eslint-disable-next-line no-undef
21+
undefinedFunction();
22+
});
23+
24+
expect(eventData.exception.values[0].type).toBe('ReferenceError');
25+
expect(eventData.exception.values[0].value).toBe(
26+
browserName === 'webkit' ? `Can't find variable: undefinedFunction` : 'undefinedFunction is not defined',
27+
);
28+
});
29+
30+
test('captures unhandled promise rejection', async ({ page }) => {
31+
const eventData = await getSentryEvent(page, () => {
32+
return Promise.reject('Rejected');
33+
});
34+
35+
expect(eventData.exception.values[0].type).toBe('UnhandledRejection');
36+
expect(eventData.exception.values[0].value).toBe('Non-Error promise rejection captured with value: Rejected');
37+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
function waitForXHR(xhr, cb) {
2+
if (xhr.readyState === 4) {
3+
return cb();
4+
}
5+
6+
setTimeout(function() {
7+
waitForXHR(xhr, cb);
8+
}, 1000 / 60);
9+
}
10+
11+
async function runInSandbox(page, fn, globals = {}) {
12+
for (const [globalName, globalVal] of Object.entries(globals)) {
13+
// Only need to pass a function for the PoC, so, we'll probably need to improve this to support other global types.
14+
await page.addScriptTag({ content: `var ${globalName} = ${globalVal};` });
15+
}
16+
17+
await page.addScriptTag({ content: `(${fn})();` });
18+
}
19+
20+
async function getSentryEvent(page, fn, globals) {
21+
const request = (
22+
await Promise.all([runInSandbox(page, fn, globals), page.waitForRequest(/.*.sentry\.io\/api.*/gm)])
23+
)[1];
24+
return JSON.parse(request.postData());
25+
}
26+
27+
module.exports = {
28+
waitForXHR,
29+
runInSandbox,
30+
getSentryEvent,
31+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const path = require('path');
2+
3+
module.exports = {
4+
mode: 'none',
5+
entry: path.resolve(__dirname, './src/index.js'),
6+
module: {
7+
rules: [
8+
{
9+
test: /\.(js)$/,
10+
exclude: /node_modules/,
11+
use: ['babel-loader'],
12+
},
13+
],
14+
},
15+
resolve: {
16+
extensions: ['*', '.js'],
17+
},
18+
output: {
19+
path: __dirname + '/dist',
20+
filename: 'bundle.js',
21+
},
22+
devServer: {
23+
static: {
24+
directory: __dirname,
25+
},
26+
},
27+
stats: 'errors-only',
28+
};

0 commit comments

Comments
 (0)