Skip to content

Browser SDK Integration Tests #3989

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 34 commits into from
Dec 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
fee95e8
Add initial set of integration tests for browser SDK using Playwright.
onurtemizkan Dec 6, 2021
4947ecc
Move Playwright tests into `packages/integration-tests`
onurtemizkan Dec 6, 2021
53bb160
Rename `E2E`s to `playwright`s
onurtemizkan Dec 6, 2021
c21ca7d
Auto install browsers only on CI
onurtemizkan Dec 6, 2021
fe15d3c
Add `integration-tests` package to lerna workspaces.
onurtemizkan Dec 6, 2021
4eb4b81
Add eslint and prettier.
onurtemizkan Dec 6, 2021
c98611d
Move `clean` to `pretest`, downgrade minimum node to v10.
onurtemizkan Dec 8, 2021
b39ed2c
Merge branch 'master' of https://github.com/getsentry/sentry-javascri…
onurtemizkan Dec 8, 2021
4beddbd
Replace tests with a simple fail case.
onurtemizkan Dec 8, 2021
58ec0c3
Exclude integration-tests in package tests.
onurtemizkan Dec 8, 2021
96a844c
Resolve 2nd level @sentry deps locally on webpack
onurtemizkan Dec 8, 2021
d4d8272
Fix sample test title.
onurtemizkan Dec 8, 2021
5855699
Merge branch 'master' of https://github.com/getsentry/sentry-javascri…
onurtemizkan Dec 8, 2021
3bf8277
Add `dom` to TS libs
onurtemizkan Dec 9, 2021
60ab8eb
Install playwright browsers inside node_modules on CI
onurtemizkan Dec 9, 2021
e2ef53b
Remove import leftover.
onurtemizkan Dec 10, 2021
0d93aff
Merge branch 'master' into onur/playwright-browser-tests
onurtemizkan Dec 10, 2021
05fa814
Rename job for consistency
rhcarvalho Dec 10, 2021
9dc6920
Markdown formatting
rhcarvalho Dec 10, 2021
bc40012
Update packages/integration-tests/package.json
onurtemizkan Dec 13, 2021
a262fd6
Merge branch 'master' into onur/playwright-browser-tests
onurtemizkan Dec 13, 2021
0633085
Update packages/integration-tests/utils/fixtures.ts
onurtemizkan Dec 13, 2021
26352af
Exclude `dist` from eslint checks.
onurtemizkan Dec 13, 2021
26ca39f
Address review suggestions.
onurtemizkan Dec 13, 2021
e668345
Remove repeated browser installation
onurtemizkan Dec 14, 2021
022faff
Remove `browserHelpers`
onurtemizkan Dec 14, 2021
4b25eb4
Merge branch 'onur/playwright-browser-tests' of https://github.com/ge…
onurtemizkan Dec 14, 2021
06c95fb
Rename project as `@sentry-internal/browser-integration-tests`.
onurtemizkan Dec 14, 2021
c3d35b6
Remove obsolete `ts-loader`, enable TS type checks before tests.
onurtemizkan Dec 14, 2021
84138d2
Merge branch 'master' of https://github.com/getsentry/sentry-javascri…
onurtemizkan Dec 14, 2021
5d7601a
Use `test:ci` on GitHub actions.
onurtemizkan Dec 14, 2021
9faaa47
Install webkit deps on run.
onurtemizkan Dec 14, 2021
84f529b
Flip sample test to pass.
onurtemizkan Dec 14, 2021
089a8eb
Merge branch 'master' into onur/playwright-browser-tests
onurtemizkan Dec 14, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,33 @@ jobs:
${{ github.workspace }}/packages/**/*.tgz
${{ github.workspace }}/packages/serverless/dist-awslambda-layer/*.zip

job_browser_playwright_tests:
name: Browser Playwright Tests
needs: job_build
runs-on: ubuntu-latest
steps:
- name: Check out current commit (${{ github.sha }})
uses: actions/checkout@v2
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: '16'
- name: Check dependency cache
uses: actions/cache@v2
with:
path: ${{ env.CACHED_DEPENDENCY_PATHS }}
key: ${{ needs.job_build.outputs.dependency_cache_key }}
- name: Check build cache
uses: actions/cache@v2
with:
path: ${{ env.CACHED_BUILD_PATHS }}
key: ${{ env.BUILD_CACHE_KEY }}
- name: Run Playwright tests
run: |
cd packages/integration-tests
yarn run playwright install-deps webkit
yarn test:ci

job_browser_integration_tests:
name: Browser Integration Tests (${{ matrix.browser }})
needs: job_build
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"pack:changed": "lerna run pack --since",
"prepublishOnly": "lerna run --stream --concurrency 1 prepublishOnly",
"postpublish": "make publish-docs && lerna run --stream --concurrency 1 postpublish",
"test": "lerna run --stream --concurrency 1 --sort test"
"test": "lerna run --ignore @sentry-internal/browser-integration-tests --stream --concurrency 1 --sort test"
},
"volta": {
"node": "14.17.0",
Expand All @@ -34,6 +34,7 @@
"packages/eslint-plugin-sdk",
"packages/gatsby",
"packages/hub",
"packages/integration-tests",
"packages/integrations",
"packages/minimal",
"packages/nextjs",
Expand Down
2 changes: 1 addition & 1 deletion packages/eslint-config-sdk/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ module.exports = {
env: {
jest: true,
},
files: ['*.test.ts', '*.test.tsx', '*.test.js', '*.test.jsx', 'test/**/*.ts', 'test/**/*.js'],
files: ['test.ts', '*.test.ts', '*.test.tsx', '*.test.js', '*.test.jsx', 'test/**/*.ts', 'test/**/*.js'],
rules: {
'max-lines': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
Expand Down
11 changes: 11 additions & 0 deletions packages/integration-tests/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
env: {
browser: true,
node: true,
},
extends: ['../../.eslintrc.js'],
ignorePatterns: ['suites/**/subject.js', 'suites/**/dist/*'],
parserOptions: {
sourceType: 'module',
},
};
1 change: 1 addition & 0 deletions packages/integration-tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
72 changes: 72 additions & 0 deletions packages/integration-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Integration Tests for Sentry Browser SDK

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.

## Structure

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.

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.

`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`.

`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.

```
suites/
|---- breadcrumbs/
|---- template.hbs [fallback template for breadcrumb tests]
|---- init.js [fallback init for breadcrumb tests]
|---- subject.js [optional fallback subject for breadcrumb tests]
|---- click_event_tree/
|---- template.hbs [optional case specific template]
|---- init.js [optional case specific init]
|---- subject.js [optional case specific subject]
|---- test.ts [assertions]
```

## Writing Tests

### Helpers

`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.

### Fixtures

[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.

## Running Tests Locally

Tests can be run locally using the latest version of Chromium with:

`yarn test`

To run tests with a different browser such as `firefox` or `webkit`:

`yarn test --browser='firefox'`
`yarn test --browser='webkit'`

Or to run on all three browsers:

`yarn test --browser='all'`

To filter tests by their title:

`yarn test -g "XMLHttpRequest without any handlers set"`

You can refer to [Playwright documentation](https://playwright.dev/docs/test-cli) for other CLI options.

### Troubleshooting

Apart from [Playwright-specific issues](https://playwright.dev/docs/troubleshooting), below are common issues that might occur while writing tests for Sentry Browser SDK.

- #### Flaky Tests
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.

- #### Build Errors
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:

- If both default `template.hbs` and `init.js` are defined for the test group.
- If a `subject.js` is defined for the test case.
- If either of `init.js` or `subject.js` contain non-browser code.
- If the webpack configuration is valid.
30 changes: 30 additions & 0 deletions packages/integration-tests/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@sentry-internal/browser-integration-tests",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"engines": {
"node": ">=10"
},
"private": true,
"scripts": {
"clean": "rimraf -g suites/**/dist",
"install-browsers": "playwright install --with-deps",
"lint": "run-s lint:prettier lint:eslint",
"lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish",
"lint:prettier": "prettier --check \"{suites,utils}/**/*.ts\"",
"test:ci": "playwright test ./suites --browser='all' --reporter='line'",
"type-check": "tsc",
"pretest": "yarn clean && yarn type-check",
"test": "playwright test ./suites"
},
"dependencies": {
"@playwright/test": "^1.17.0",
"babel-loader": "^8.2.2",
"handlebars-loader": "^1.7.1",
"html-webpack-plugin": "^5.5.0",
"playwright": "^1.17.1",
"typescript": "^4.5.2",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to stick with 3.7.5 to match with the rest of the codebase. Can we remove this?

Suggested change
"typescript": "^4.5.2",

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AbhiPrasad, @playwright/test uses export type syntax for its types (and we're importing them). It's not a problem while running tests, but addressing #3989 (comment), to run a type check without compiling before tests, we need a newer version of TS. We also no longer need ts-loader as init and subject scripts are not TS. So the only use of TS here will be static type checking. So is that fine to keep this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For ref: c3d35b6

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked with @AbhiPrasad, and we agreed we can address this later in a follow up.

"webpack": "^5.52.0"
}
}
8 changes: 8 additions & 0 deletions packages/integration-tests/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { PlaywrightTestConfig } from '@playwright/test';

const config: PlaywrightTestConfig = {
retries: 2,
timeout: 12000,
workers: 3,
};
export default config;
7 changes: 7 additions & 0 deletions packages/integration-tests/suites/demo/init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://[email protected]/1337',
});
11 changes: 11 additions & 0 deletions packages/integration-tests/suites/demo/template.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<script src="{{htmlWebpackPlugin.options.initialization}}"></script>
</head>
<body>
<script src="{{htmlWebpackPlugin.options.subject}}"></script>
</body>
</html>
1 change: 1 addition & 0 deletions packages/integration-tests/suites/demo/tmp/subject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Sentry.captureMessage(1);
12 changes: 12 additions & 0 deletions packages/integration-tests/suites/demo/tmp/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { expect } from '@playwright/test';

import { sentryTest } from '../../../utils/fixtures';
import { getSentryRequest } from '../../../utils/helpers';

sentryTest('should fail', async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });

const eventData = await getSentryRequest(page, url);

expect(eventData.message).toBe('1');
});
12 changes: 12 additions & 0 deletions packages/integration-tests/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",

"compilerOptions": {
"lib": ["dom", "es2019"],
"moduleResolution": "node",
"noEmit": true,
"strict": true
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}
46 changes: 46 additions & 0 deletions packages/integration-tests/utils/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { test as base } from '@playwright/test';
import fs from 'fs';
import path from 'path';

import { generatePage } from './generatePage';

const getAsset = (assetDir: string, asset: string): string => {
const assetPath = `${assetDir}/${asset}`;

if (fs.existsSync(assetPath)) {
return assetPath;
}

return `${path.dirname(assetDir)}/${asset}`;
};

export type TestOptions = {
testDir: string;
};

export type TestFixtures = {
testDir: string;
getLocalTestPath: (options: TestOptions) => Promise<string>;
};

const sentryTest = base.extend<TestFixtures>({
// eslint-disable-next-line no-empty-pattern
getLocalTestPath: ({}, use, testInfo) => {
return use(async ({ testDir }) => {
const pagePath = `file:///${path.resolve(testDir, './dist/index.html')}`;

// Build test page if it doesn't exist
if (!fs.existsSync(pagePath)) {
const testDir = path.dirname(testInfo.file);
const subject = getAsset(testDir, 'subject.js');
const template = getAsset(testDir, 'template.hbs');
const init = getAsset(testDir, 'init.js');

await generatePage(init, subject, template, testDir);
}
return pagePath;
});
},
});

export { sentryTest };
89 changes: 89 additions & 0 deletions packages/integration-tests/utils/generatePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Package } from '@sentry/types';
import { existsSync, mkdirSync, promises } from 'fs';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import path from 'path';
import webpack from 'webpack';

import webpackConfig from '../webpack.config';

const PACKAGE_PATH = '../../packages';

/**
* Generate webpack aliases based on packages in monorepo
* Example of an alias: '@sentry/serverless': 'path/to/sentry-javascript/packages/serverless',
*/
async function generateSentryAlias(): Promise<Record<string, string>> {
const dirents = (await promises.readdir(PACKAGE_PATH, { withFileTypes: true }))
.filter(dirent => dirent.isDirectory())
.map(dir => dir.name);

return Object.fromEntries(
await Promise.all(
dirents.map(async d => {
const packageJSON: Package = JSON.parse(
(await promises.readFile(path.resolve(PACKAGE_PATH, d, 'package.json'), { encoding: 'utf-8' })).toString(),
);
return [packageJSON['name'], path.resolve(PACKAGE_PATH, d)];
}),
),
);
}

export async function generatePage(
initializationPath: string,
subjectPath: string,
templatePath: string,
outPath: string,
): Promise<void> {
const localPath = `${outPath}/dist`;
const bundlePath = `${localPath}/index.html`;

const alias = await generateSentryAlias();

if (!existsSync(localPath)) {
mkdirSync(localPath, { recursive: true });
}

if (!existsSync(bundlePath)) {
await new Promise<void>((resolve, reject) => {
const compiler = webpack(
webpackConfig({
resolve: {
alias,
},
entry: {
initialization: initializationPath,
subject: subjectPath,
},
output: {
path: localPath,
filename: '[name].bundle.js',
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: templatePath,
initialization: 'initialization.bundle.js',
subject: `subject.bundle.js`,
inject: false,
}),
],
}),
);

compiler.run(err => {
if (err) {
reject(err);
}

compiler.close(err => {
if (err) {
reject(err);
}

resolve();
});
});
});
}
}
Loading