Skip to content

Commit c258816

Browse files
onurtemizkanrhcarvalhoAbhiPrasad
committed
feat: Browser SDK Integration Tests (#3989)
Initial structure of new integration tests for Sentry's Browser SDK. New integration tests internally use Playwright and run on recent versions of Chromium, Webkit and Firefox linked with the Playwright release. This new test structure aims to create a modern and intuitive environment to test @sentry/browser and potentially other browser-side SDKs like @sentry/react and @sentry/vue. Co-authored-by: Rodolfo Carvalho <[email protected]> Co-authored-by: Abhijeet Prasad <[email protected]>
1 parent eae2f9a commit c258816

File tree

19 files changed

+3189
-16
lines changed

19 files changed

+3189
-16
lines changed

.github/workflows/build.yml

+27
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,33 @@ jobs:
271271
${{ github.workspace }}/packages/**/*.tgz
272272
${{ github.workspace }}/packages/serverless/dist-awslambda-layer/*.zip
273273
274+
job_browser_playwright_tests:
275+
name: Browser Playwright Tests
276+
needs: job_build
277+
runs-on: ubuntu-latest
278+
steps:
279+
- name: Check out current commit (${{ github.sha }})
280+
uses: actions/checkout@v2
281+
- name: Set up Node
282+
uses: actions/setup-node@v1
283+
with:
284+
node-version: '16'
285+
- name: Check dependency cache
286+
uses: actions/cache@v2
287+
with:
288+
path: ${{ env.CACHED_DEPENDENCY_PATHS }}
289+
key: ${{ needs.job_build.outputs.dependency_cache_key }}
290+
- name: Check build cache
291+
uses: actions/cache@v2
292+
with:
293+
path: ${{ env.CACHED_BUILD_PATHS }}
294+
key: ${{ env.BUILD_CACHE_KEY }}
295+
- name: Run Playwright tests
296+
run: |
297+
cd packages/integration-tests
298+
yarn run playwright install-deps webkit
299+
yarn test:ci
300+
274301
job_browser_integration_tests:
275302
name: Browser Integration Tests (${{ matrix.browser }})
276303
needs: job_build

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"pack:changed": "lerna run pack --since",
2020
"prepublishOnly": "lerna run --stream --concurrency 1 prepublishOnly",
2121
"postpublish": "make publish-docs && lerna run --stream --concurrency 1 postpublish",
22-
"test": "lerna run --stream --concurrency 1 --sort test"
22+
"test": "lerna run --ignore @sentry-internal/browser-integration-tests --stream --concurrency 1 --sort test"
2323
},
2424
"volta": {
2525
"node": "14.17.0",
@@ -34,6 +34,7 @@
3434
"packages/eslint-plugin-sdk",
3535
"packages/gatsby",
3636
"packages/hub",
37+
"packages/integration-tests",
3738
"packages/integrations",
3839
"packages/minimal",
3940
"packages/nextjs",

packages/eslint-config-sdk/src/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ module.exports = {
164164
env: {
165165
jest: true,
166166
},
167-
files: ['*.test.ts', '*.test.tsx', '*.test.js', '*.test.jsx', 'test/**/*.ts', 'test/**/*.js'],
167+
files: ['test.ts', '*.test.ts', '*.test.tsx', '*.test.js', '*.test.jsx', 'test/**/*.ts', 'test/**/*.js'],
168168
rules: {
169169
'max-lines': 'off',
170170
'@typescript-eslint/explicit-function-return-type': 'off',
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module.exports = {
2+
env: {
3+
browser: true,
4+
node: true,
5+
},
6+
extends: ['../../.eslintrc.js'],
7+
ignorePatterns: ['suites/**/subject.js', 'suites/**/dist/*'],
8+
parserOptions: {
9+
sourceType: 'module',
10+
},
11+
};

packages/integration-tests/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist

packages/integration-tests/README.md

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Integration Tests for Sentry Browser SDK
2+
3+
Integration tests for Sentry's Browser SDK use [Playwright](https://playwright.dev/) internally. These tests are run on latest stable versions of Chromium, Firefox and Webkit.
4+
5+
## Structure
6+
7+
The tests are grouped by their scope such as `breadcrumbs` or `onunhandledrejection`. In every group of tests, there are multiple folders containing test cases with their optional supporting assets.
8+
9+
Each case group has a default HTML skeleton named `template.hbs`, and also a default initialization script named `init.js `, which contains the `Sentry.init()` call. These defaults are used as fallbacks when a specific `template.hbs` or `init.js` is not defined in a case folder.
10+
11+
`subject.js` contains the logic that sets up the environment to be tested. It also can be defined locally and as a group fallback. Unlike `template.hbs` and `init.js`, it's not required to be defined for a group, as there may be cases that does not require a subject, instead the logic is injected using `injectScriptAndGetEvents` from `utils/helpers.ts`.
12+
13+
`test.ts` is required for each test case, which contains the assertions (and if required the script injection logic). For every case, any set of `init.js`, `template.hbs` and `subject.js` can be defined locally, and each one of them will have precedence over the default definitions of the test group.
14+
15+
```
16+
suites/
17+
|---- breadcrumbs/
18+
|---- template.hbs [fallback template for breadcrumb tests]
19+
|---- init.js [fallback init for breadcrumb tests]
20+
|---- subject.js [optional fallback subject for breadcrumb tests]
21+
|---- click_event_tree/
22+
|---- template.hbs [optional case specific template]
23+
|---- init.js [optional case specific init]
24+
|---- subject.js [optional case specific subject]
25+
|---- test.ts [assertions]
26+
```
27+
28+
## Writing Tests
29+
30+
### Helpers
31+
32+
`utils/helpers.ts` contains helpers that could be used in assertions (`test.ts`). These helpers define a convenient and reliable API to interact with Playwright's native API. It's highly recommended to define all common patterns of Playwright usage in helpers.
33+
34+
### Fixtures
35+
36+
[Fixtures](https://playwright.dev/docs/api/class-fixtures) allows us to define the globals and test-specific information in assertion groups (`test.ts` files). In it's current state, `fixtures.ts` contains an extension over the pure version of `test()` function of Playwright. All the tests should import `sentryTest` function from `utils/fixtures.ts` instead of `@playwright/test` to be able to access the extra fixtures.
37+
38+
## Running Tests Locally
39+
40+
Tests can be run locally using the latest version of Chromium with:
41+
42+
`yarn test`
43+
44+
To run tests with a different browser such as `firefox` or `webkit`:
45+
46+
`yarn test --browser='firefox'`
47+
`yarn test --browser='webkit'`
48+
49+
Or to run on all three browsers:
50+
51+
`yarn test --browser='all'`
52+
53+
To filter tests by their title:
54+
55+
`yarn test -g "XMLHttpRequest without any handlers set"`
56+
57+
You can refer to [Playwright documentation](https://playwright.dev/docs/test-cli) for other CLI options.
58+
59+
### Troubleshooting
60+
61+
Apart from [Playwright-specific issues](https://playwright.dev/docs/troubleshooting), below are common issues that might occur while writing tests for Sentry Browser SDK.
62+
63+
- #### Flaky Tests
64+
If a test fails randomly, giving a `Page Closed`, `Target Closed` or a similar error, most of the times, the reason is a race condition between the page action defined in the `subject` and the listeners of the Sentry event / request. It's recommended to firstly check `utils/helpers.ts` whether if that async logic can be replaced by one of the helpers. If not, whether the awaited (or non-awaited on purpose in some cases) Playwright methods can be orchestrated by [`Promise.all`](http://mdn.io/promise.all). Manually-defined waiting logic such as timeouts are not recommended, and should not be required in most of the cases.
65+
66+
- #### Build Errors
67+
Before running, a page for each test case is built under the case folder inside `dist`. If a page build is failed, it's recommended to check:
68+
69+
- If both default `template.hbs` and `init.js` are defined for the test group.
70+
- If a `subject.js` is defined for the test case.
71+
- If either of `init.js` or `subject.js` contain non-browser code.
72+
- If the webpack configuration is valid.
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "@sentry-internal/browser-integration-tests",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"engines": {
7+
"node": ">=10"
8+
},
9+
"private": true,
10+
"scripts": {
11+
"clean": "rimraf -g suites/**/dist",
12+
"install-browsers": "playwright install --with-deps",
13+
"lint": "run-s lint:prettier lint:eslint",
14+
"lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish",
15+
"lint:prettier": "prettier --check \"{suites,utils}/**/*.ts\"",
16+
"test:ci": "playwright test ./suites --browser='all' --reporter='line'",
17+
"type-check": "tsc",
18+
"pretest": "yarn clean && yarn type-check",
19+
"test": "playwright test ./suites"
20+
},
21+
"dependencies": {
22+
"@playwright/test": "^1.17.0",
23+
"babel-loader": "^8.2.2",
24+
"handlebars-loader": "^1.7.1",
25+
"html-webpack-plugin": "^5.5.0",
26+
"playwright": "^1.17.1",
27+
"typescript": "^4.5.2",
28+
"webpack": "^5.52.0"
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { PlaywrightTestConfig } from '@playwright/test';
2+
3+
const config: PlaywrightTestConfig = {
4+
retries: 2,
5+
timeout: 12000,
6+
workers: 3,
7+
};
8+
export default config;
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,11 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<title></title>
6+
<script src="{{htmlWebpackPlugin.options.initialization}}"></script>
7+
</head>
8+
<body>
9+
<script src="{{htmlWebpackPlugin.options.subject}}"></script>
10+
</body>
11+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Sentry.captureMessage(1);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../utils/fixtures';
4+
import { getSentryRequest } from '../../../utils/helpers';
5+
6+
sentryTest('should fail', async ({ getLocalTestPath, page }) => {
7+
const url = await getLocalTestPath({ testDir: __dirname });
8+
9+
const eventData = await getSentryRequest(page, url);
10+
11+
expect(eventData.message).toBe('1');
12+
});
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
4+
"compilerOptions": {
5+
"lib": ["dom", "es2019"],
6+
"moduleResolution": "node",
7+
"noEmit": true,
8+
"strict": true
9+
},
10+
"include": ["**/*.ts"],
11+
"exclude": ["node_modules"]
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { test as base } from '@playwright/test';
2+
import fs from 'fs';
3+
import path from 'path';
4+
5+
import { generatePage } from './generatePage';
6+
7+
const getAsset = (assetDir: string, asset: string): string => {
8+
const assetPath = `${assetDir}/${asset}`;
9+
10+
if (fs.existsSync(assetPath)) {
11+
return assetPath;
12+
}
13+
14+
return `${path.dirname(assetDir)}/${asset}`;
15+
};
16+
17+
export type TestOptions = {
18+
testDir: string;
19+
};
20+
21+
export type TestFixtures = {
22+
testDir: string;
23+
getLocalTestPath: (options: TestOptions) => Promise<string>;
24+
};
25+
26+
const sentryTest = base.extend<TestFixtures>({
27+
// eslint-disable-next-line no-empty-pattern
28+
getLocalTestPath: ({}, use, testInfo) => {
29+
return use(async ({ testDir }) => {
30+
const pagePath = `file:///${path.resolve(testDir, './dist/index.html')}`;
31+
32+
// Build test page if it doesn't exist
33+
if (!fs.existsSync(pagePath)) {
34+
const testDir = path.dirname(testInfo.file);
35+
const subject = getAsset(testDir, 'subject.js');
36+
const template = getAsset(testDir, 'template.hbs');
37+
const init = getAsset(testDir, 'init.js');
38+
39+
await generatePage(init, subject, template, testDir);
40+
}
41+
return pagePath;
42+
});
43+
},
44+
});
45+
46+
export { sentryTest };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { Package } from '@sentry/types';
2+
import { existsSync, mkdirSync, promises } from 'fs';
3+
import HtmlWebpackPlugin from 'html-webpack-plugin';
4+
import path from 'path';
5+
import webpack from 'webpack';
6+
7+
import webpackConfig from '../webpack.config';
8+
9+
const PACKAGE_PATH = '../../packages';
10+
11+
/**
12+
* Generate webpack aliases based on packages in monorepo
13+
* Example of an alias: '@sentry/serverless': 'path/to/sentry-javascript/packages/serverless',
14+
*/
15+
async function generateSentryAlias(): Promise<Record<string, string>> {
16+
const dirents = (await promises.readdir(PACKAGE_PATH, { withFileTypes: true }))
17+
.filter(dirent => dirent.isDirectory())
18+
.map(dir => dir.name);
19+
20+
return Object.fromEntries(
21+
await Promise.all(
22+
dirents.map(async d => {
23+
const packageJSON: Package = JSON.parse(
24+
(await promises.readFile(path.resolve(PACKAGE_PATH, d, 'package.json'), { encoding: 'utf-8' })).toString(),
25+
);
26+
return [packageJSON['name'], path.resolve(PACKAGE_PATH, d)];
27+
}),
28+
),
29+
);
30+
}
31+
32+
export async function generatePage(
33+
initializationPath: string,
34+
subjectPath: string,
35+
templatePath: string,
36+
outPath: string,
37+
): Promise<void> {
38+
const localPath = `${outPath}/dist`;
39+
const bundlePath = `${localPath}/index.html`;
40+
41+
const alias = await generateSentryAlias();
42+
43+
if (!existsSync(localPath)) {
44+
mkdirSync(localPath, { recursive: true });
45+
}
46+
47+
if (!existsSync(bundlePath)) {
48+
await new Promise<void>((resolve, reject) => {
49+
const compiler = webpack(
50+
webpackConfig({
51+
resolve: {
52+
alias,
53+
},
54+
entry: {
55+
initialization: initializationPath,
56+
subject: subjectPath,
57+
},
58+
output: {
59+
path: localPath,
60+
filename: '[name].bundle.js',
61+
},
62+
plugins: [
63+
new HtmlWebpackPlugin({
64+
filename: 'index.html',
65+
template: templatePath,
66+
initialization: 'initialization.bundle.js',
67+
subject: `subject.bundle.js`,
68+
inject: false,
69+
}),
70+
],
71+
}),
72+
);
73+
74+
compiler.run(err => {
75+
if (err) {
76+
reject(err);
77+
}
78+
79+
compiler.close(err => {
80+
if (err) {
81+
reject(err);
82+
}
83+
84+
resolve();
85+
});
86+
});
87+
});
88+
}
89+
}

0 commit comments

Comments
 (0)