Tests: Adds end-to-end tests skeleton and basic smoke test scenario (#16901)

* Chore: Adds neccessary packages

* Wip: Initial dummy test in place

* Feature: Downloads Chromium if needed

* Fix: Adds global config object

* Refactor: Adds basic e2eScenario

* Build: Adds end to end tests to config

* Build: Changes end to end job

* Build: Adds browsers to image

* Build: Adds failing test

* Refactor: Adds first e2e-test scenario

* Fix: Ignores test output in gitignore

* Refactor: Adds compare screenshots ability

* Refactor: Removes unnecessary code

* Build: Removes jest-puppeteer

* Fix: Replaces test snapshots

* Refactor: Creates output dir if missing

* Refactor: Changes aria-labels to be more consistent

* Docs: Adds section about end to end tests

* Fix: Fixes snapshots

* Docs: Adds information about ENV variables
pull/13086/head
Hugo Häggmark 6 years ago committed by GitHub
parent ceb9f0855b
commit a4d287d2e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      .circleci/config.yml
  2. 2
      .gitignore
  3. 35
      README.md
  4. 15
      jest.config.e2e.js
  5. 13
      package.json
  6. 2
      public/app/core/components/search/search_results.html
  7. 9
      public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx
  8. 2
      public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap
  9. 6
      public/app/features/dashboard/components/DashNav/DashNavButton.tsx
  10. 11
      public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts
  11. 2
      public/app/features/dashboard/components/ShareModal/template.html
  12. 7
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx
  13. 4
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx
  14. 2
      public/app/features/dashboard/panel_editor/PanelEditor.tsx
  15. 1
      public/app/features/datasources/NewDataSourcePage.tsx
  16. 8
      public/app/features/datasources/settings/ButtonRow.tsx
  17. 6
      public/app/features/datasources/settings/DataSourceSettingsPage.tsx
  18. 1
      public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap
  19. 2
      public/app/features/panel/panel_directive.ts
  20. 2
      public/app/features/panel/panel_header.ts
  21. 6
      public/app/partials/login.html
  22. 2
      public/app/plugins/datasource/testdata/partials/query.editor.html
  23. 2
      public/app/plugins/panel/graph/axes_editor.html
  24. 6
      public/e2e-test/core/constants.ts
  25. 51
      public/e2e-test/core/images.ts
  26. 29
      public/e2e-test/core/launcher.ts
  27. 22
      public/e2e-test/core/login.ts
  28. 84
      public/e2e-test/core/pageObjects.ts
  29. 110
      public/e2e-test/core/pages.ts
  30. 30
      public/e2e-test/core/scenario.ts
  31. 22
      public/e2e-test/install/install.ts
  32. 13
      public/e2e-test/pages/dashboards/createDashboardPage.ts
  33. 14
      public/e2e-test/pages/dashboards/dashboardsPage.ts
  34. 20
      public/e2e-test/pages/dashboards/saveDashboardModal.ts
  35. 13
      public/e2e-test/pages/datasources/addDataSourcePage.ts
  36. 7
      public/e2e-test/pages/datasources/dataSources.ts
  37. 22
      public/e2e-test/pages/datasources/editDataSourcePage.ts
  38. 26
      public/e2e-test/pages/panels/editPanel.ts
  39. 14
      public/e2e-test/pages/panels/panel.ts
  40. 12
      public/e2e-test/pages/panels/sharePanelModal.ts
  41. 23
      public/e2e-test/pages/start/loginPage.ts
  42. 85
      public/e2e-test/scenarios/smoke.test.ts
  43. BIN
      public/e2e-test/screenShots/theTruth/smoke-test-scenario.png
  44. 8
      tsconfig.json
  45. 93
      yarn.lock

@ -69,6 +69,28 @@ jobs:
- run:
name: cache server tests
command: './scripts/circle-test-cache-servers.sh'
end-to-end-test:
docker:
- image: circleci/node:8-browsers
- image: grafana/grafana:master
steps:
- run: dockerize -wait tcp://127.0.0.1:3000 -timeout 120s
- checkout
- restore_cache:
key: dependency-cache-{{ checksum "yarn.lock" }}
- run:
name: yarn install
command: 'yarn install --pure-lockfile --no-progress'
no_output_timeout: 5m
- save_cache:
key: dependency-cache-{{ checksum "yarn.lock" }}
paths:
- node_modules
- run:
name: run end-to-end tests
command: 'env BASE_URL=http://127.0.0.1:3000 yarn e2e-tests'
no_output_timeout: 5m
codespell:
docker:

2
.gitignore vendored

@ -84,3 +84,5 @@ debug.test
/packages/**/dist
/packages/**/compiled
/packages/**/.rpt2_cache
theOutput/

@ -1,5 +1,5 @@
[Grafana](https://grafana.com) [![Circle CI](https://circleci.com/gh/grafana/grafana.svg?style=svg)](https://circleci.com/gh/grafana/grafana) [![Go Report Card](https://goreportcard.com/badge/github.com/grafana/grafana)](https://goreportcard.com/report/github.com/grafana/grafana) [![codecov](https://codecov.io/gh/grafana/grafana/branch/master/graph/badge.svg)](https://codecov.io/gh/grafana/grafana)
================
# [Grafana](https://grafana.com) [![Circle CI](https://circleci.com/gh/grafana/grafana.svg?style=svg)](https://circleci.com/gh/grafana/grafana) [![Go Report Card](https://goreportcard.com/badge/github.com/grafana/grafana)](https://goreportcard.com/report/github.com/grafana/grafana) [![codecov](https://codecov.io/gh/grafana/grafana/branch/master/graph/badge.svg)](https://codecov.io/gh/grafana/grafana)
[Website](https://grafana.com) |
[Twitter](https://twitter.com/grafana) |
[Community & Forum](https://community.grafana.com)
@ -12,12 +12,15 @@ Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
-->
## Installation
Head to [docs.grafana.org](http://docs.grafana.org/installation/) for documentation or [download](https://grafana.com/get) to get the latest release.
## Documentation & Support
Be sure to read the [getting started guide](http://docs.grafana.org/guides/gettingstarted/) and the other feature guides.
## Run from master
If you want to build a package yourself, or contribute - here is a guide for how to do that. You can always find
the latest master builds [here](https://grafana.com/grafana/download)
@ -48,7 +51,7 @@ go run build.go build
#### Frontend assets
*For this you need Node.js (LTS version).*
_For this you need Node.js (LTS version)._
```bash
yarn install --pure-lockfile
@ -80,7 +83,7 @@ yarn start:hot
env GRAFANA_THEME=light yarn start:hot
```
*Note: HMR for Angular is not supported. If you edit files in the Angular part of the app, the whole page will reload.*
_Note: HMR for Angular is not supported. If you edit files in the Angular part of the app, the whole page will reload._
Run tests and rebuild on source change:
@ -128,7 +131,9 @@ In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode =
### Running tests
#### Frontend
Execute all frontend tests
```bash
yarn test
```
@ -139,6 +144,7 @@ Writing & watching frontend tests
- Jest will run all test files that end with the name ".test.ts"
#### Backend
```bash
# Run Golang tests using sqlite3 as database (default)
go test ./pkg/...
@ -150,6 +156,26 @@ GRAFANA_TEST_DB=mysql go test ./pkg/...
GRAFANA_TEST_DB=postgres go test ./pkg/...
```
#### End-to-end
Execute all end-to-end tests
```bash
yarn e2e-tests
```
Execute all end-to-end tests using using a specific url
```bash
ENV BASE_URL=http://localhost:3333 yarn e2e-tests
```
Debugging all end-to-end tests (BROWSER=1 will start the browser and SLOWMO=1 will delay each puppeteer operation by 100ms)
```bash
ENV BROWSER=1 SLOWMO=1 yarn e2e-tests
```
### Datasource and dashboard provisioning
[Here](https://github.com/grafana/grafana/tree/master/devenv) you can find helpful scripts and docker-compose setup
@ -171,4 +197,3 @@ plugin development.
## License
Grafana is distributed under [Apache 2.0 License](https://github.com/grafana/grafana/blob/master/LICENSE).

@ -0,0 +1,15 @@
require('module-alias/register');
module.exports = {
verbose: false,
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
moduleDirectories: ['node_modules', 'public'],
roots: ['<rootDir>/public/e2e-test'],
testRegex: '(\\.|/)(test)\\.(jsx?|tsx?)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
setupFiles: [],
globals: { 'ts-jest': { isolatedModules: true } },
setupFilesAfterEnv: ['expect-puppeteer', '<rootDir>/public/e2e-test/install/install.ts'],
};

@ -25,12 +25,16 @@
"@types/commander": "2.12.2",
"@types/d3": "4.13.1",
"@types/enzyme": "3.9.0",
"@types/expect-puppeteer": "3.3.1",
"@types/inquirer": "0.0.43",
"@types/jest": "24.0.11",
"@types/jquery": "1.10.35",
"@types/lodash": "4.14.123",
"@types/node": "11.13.4",
"@types/papaparse": "4.5.9",
"@types/pixelmatch": "4.0.0",
"@types/pngjs": "3.3.2",
"@types/puppeteer-core": "1.9.0",
"@types/react": "16.8.16",
"@types/react-dom": "16.8.4",
"@types/react-grid-layout": "0.16.7",
@ -55,6 +59,7 @@
"es6-promise": "3.3.1",
"es6-shim": "0.35.5",
"execa": "1.0.0",
"expect-puppeteer": "4.1.1",
"expect.js": "0.2.0",
"expose-loader": "0.7.5",
"file-loader": "3.0.1",
@ -85,6 +90,7 @@
"load-grunt-tasks": "3.5.2",
"mini-css-extract-plugin": "0.5.0",
"mocha": "4.1.0",
"module-alias": "2.2.0",
"monaco-editor": "0.15.6",
"ng-annotate-loader": "0.6.1",
"ng-annotate-webpack-plugin": "0.3.0",
@ -94,10 +100,13 @@
"optimize-css-assets-webpack-plugin": "5.0.1",
"ora": "3.2.0",
"phantomjs-prebuilt": "2.1.16",
"pixelmatch": "4.0.2",
"pngjs": "3.4.0",
"postcss-browser-reporter": "0.5.0",
"postcss-loader": "3.0.0",
"postcss-reporter": "6.0.1",
"prettier": "1.16.4",
"puppeteer-core": "1.15.0",
"react-hooks-testing-library": "0.3.7",
"react-hot-loader": "4.8.0",
"react-test-renderer": "16.8.4",
@ -140,6 +149,7 @@
"tslint": "tslint -c tslint.json --project tsconfig.json",
"typecheck": "tsc --noEmit",
"jest": "jest --notify --watch",
"e2e-tests": "jest --runInBand --config=jest.config.e2e.js",
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
"storybook": "cd packages/grafana-ui && yarn storybook",
"storybook:build": "cd packages/grafana-ui && yarn storybook:build",
@ -242,5 +252,8 @@
"**/@types/*",
"**/@types/*/**"
]
},
"_moduleAliases": {
"puppeteer": "node_modules/puppeteer-core"
}
}

@ -20,7 +20,7 @@
<div class="search-section__header" ng-show="section.hideHeader"></div>
<div ng-if="section.expanded">
<a ng-repeat="item in section.items" class="search-item search-item--indent" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}" >
<a ng-repeat="item in section.items" class="search-item search-item--indent" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}" aria-label="{{::item.title}}">
<div ng-click="ctrl.toggleSelection(item, $event)" class="center-vh">
<gf-form-checkbox
ng-show="ctrl.editable"

@ -132,10 +132,15 @@ export class AddPanelWidget extends React.Component<Props, State> {
dashboard.removePanel(this.props.panel);
};
renderOptionLink = (icon, text, onClick) => {
renderOptionLink = (icon: string, text: string, onClick) => {
return (
<div>
<a href="#" onClick={onClick} className="add-panel-widget__link btn btn-inverse">
<a
href="#"
onClick={onClick}
className="add-panel-widget__link btn btn-inverse"
aria-label={`${text} CTA button`}
>
<div className="add-panel-widget__icon">
<i className={`gicon gicon-${icon}`} />
</div>

@ -35,6 +35,7 @@ exports[`Render should render component 1`] = `
>
<div>
<a
aria-label="Add Query CTA button"
className="add-panel-widget__link btn btn-inverse"
href="#"
onClick={[Function]}
@ -53,6 +54,7 @@ exports[`Render should render component 1`] = `
</div>
<div>
<a
aria-label="Choose Visualization CTA button"
className="add-panel-widget__link btn btn-inverse"
href="#"
onClick={[Function]}

@ -16,7 +16,11 @@ export const DashNavButton: FunctionComponent<Props> = ({ icon, tooltip, classSu
if (onClick) {
return (
<Tooltip content={tooltip}>
<button className={`btn navbar-button navbar-button--${classSuffix}`} onClick={onClick}>
<button
className={`btn navbar-button navbar-button--${classSuffix}`}
onClick={onClick}
aria-label={`${tooltip} navbar button`}
>
<i className={icon} />
</button>
</Tooltip>

@ -17,7 +17,7 @@ const template = `
<div class="p-t-2">
<div class="gf-form">
<label class="gf-form-label width-8">New name</label>
<input type="text" class="gf-form-input" ng-model="ctrl.clone.title" give-focus="true" required>
<input type="text" class="gf-form-input" ng-model="ctrl.clone.title" give-focus="true" required aria-label="Save dashboard title field">
</div>
<folder-picker initial-folder-id="ctrl.folderId"
on-change="ctrl.onFolderChange($folder)"
@ -34,7 +34,14 @@ const template = `
</div>
<div class="gf-form-button-row text-center">
<button type="submit" class="btn btn-primary" ng-click="ctrl.save()" ng-disabled="!ctrl.isValidFolderSelection">Save</button>
<button
type="submit"
class="btn btn-primary"
ng-click="ctrl.save()"
ng-disabled="!ctrl.isValidFolderSelection"
aria-label="Save dashboard button">
Save
</button>
<a class="btn-text" ng-click="ctrl.dismiss();">Cancel</a>
</div>
</form>

@ -92,7 +92,7 @@
</div>
</div>
<div class="gf-form" ng-show="modeSharePanel">
<a href="{{imageUrl}}" target="_blank"><i class="fa fa-camera"></i> Direct link rendered image</a>
<a href="{{imageUrl}}" target="_blank" aria-label="Link to rendered image"><i class="fa fa-camera"></i> Direct link rendered image</a>
</div>
</div>
</script>

@ -90,7 +90,12 @@ export class PanelHeader extends Component<Props, State> {
error={error}
/>
<div className={panelHeaderClass}>
<div className="panel-title-container" onClick={this.onMenuToggle} onMouseDown={this.onMouseDown}>
<div
className="panel-title-container"
onClick={this.onMenuToggle}
onMouseDown={this.onMouseDown}
aria-label="Panel Title"
>
<div className="panel-title">
<span className="icon-gf panel-alert-icon" />
<span className="panel-title-text">

@ -14,7 +14,9 @@ export const PanelHeaderMenuItem: FC<Props & PanelMenuItem> = props => {
<li className={isSubMenu ? 'dropdown-submenu' : null}>
<a onClick={props.onClick}>
{props.iconClassName && <i className={props.iconClassName} />}
<span className="dropdown-item-text">{props.text}</span>
<span className="dropdown-item-text" aria-label={`${props.text} panel menu item`}>
{props.text}
</span>
{props.shortcut && <span className="dropdown-menu-item-shortcut">{props.shortcut}</span>}
</a>
{props.children}

@ -145,7 +145,7 @@ function TabItem({ tab, activeTab, onClick }: TabItemParams) {
return (
<div className="panel-editor-tabs__item" onClick={() => onClick(tab)}>
<a className={tabClasses}>
<a className={tabClasses} aria-label={`${tab.text} tab button`}>
<Tooltip content={`${tab.text}`} placement="auto">
<i className={`gicon gicon-${tab.id}${activeTab === tab.id ? '-active' : ''}`} />
</Tooltip>

@ -54,6 +54,7 @@ class NewDataSourcePage extends PureComponent<Props> {
onClick={() => this.onDataSourceTypeClicked(plugin)}
className="add-data-source-grid-item"
key={`${plugin.id}-${index}`}
aria-label={`${plugin.name} datasource plugin`}
>
<img className="add-data-source-grid-item-logo" src={plugin.info.logos.small} />
<span className="add-data-source-grid-item-text">{plugin.name}</span>

@ -12,7 +12,13 @@ const ButtonRow: FC<Props> = ({ isReadOnly, onDelete, onSubmit, onTest }) => {
return (
<div className="gf-form-button-row">
{!isReadOnly && (
<button type="submit" className="btn btn-primary" disabled={isReadOnly} onClick={event => onSubmit(event)}>
<button
type="submit"
className="btn btn-primary"
disabled={isReadOnly}
onClick={event => onSubmit(event)}
aria-label="Save and Test button"
>
Save &amp; Test
</button>
)}

@ -212,7 +212,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
<div className="gf-form-group">
{testingMessage && (
<div className={`alert-${testingStatus} alert`}>
<div className={`alert-${testingStatus} alert`} aria-label="Datasource settings page Alert">
<div className="alert-icon">
{testingStatus === 'error' ? (
<i className="fa fa-exclamation-triangle" />
@ -221,7 +221,9 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
)}
</div>
<div className="alert-body">
<div className="alert-title">{testingMessage}</div>
<div className="alert-title" aria-label="Datasource settings page Alert message">
{testingMessage}
</div>
</div>
</div>
)}

@ -33,6 +33,7 @@ exports[`Render should render with buttons enabled 1`] = `
className="gf-form-button-row"
>
<button
aria-label="Save and Test button"
className="btn btn-primary"
disabled={false}
onClick={[Function]}

@ -17,7 +17,7 @@ const panelTemplate = `
<i class="fa fa-spinner fa-spin"></i>
</span>
<panel-header class="panel-title-container" panel-ctrl="ctrl"></panel-header>
<panel-header class="panel-title-container" panel-ctrl="ctrl" aria-label="Panel Title"></panel-header>
</div>
<div class="panel-content">

@ -34,7 +34,7 @@ function renderMenuItem(item, ctrl) {
}
html += `><i class="${item.icon}"></i>`;
html += `<span class="dropdown-item-text">${item.text}</span>`;
html += `<span class="dropdown-item-text" aria-label="${item.text} panel menu item">${item.text}</span>`;
if (item.shortcut) {
html += `<span class="dropdown-menu-item-shortcut">${item.shortcut}</span>`;

@ -8,15 +8,15 @@
<div class="login-inner-box" id="login-view">
<form name="loginForm" class="login-form-group gf-form-group" ng-hide="disableLoginForm">
<div class="login-form">
<input type="text" name="username" class="gf-form-input login-form-input" required ng-model='formModel.user' placeholder={{loginHint}}
<input type="text" name="username" class="gf-form-input login-form-input" required ng-model='formModel.user' placeholder={{loginHint}} aria-label="Username input field"
autofocus autofill-event-fix>
</div>
<div class="login-form">
<input type="password" name="password" class="gf-form-input login-form-input" required ng-model="formModel.password" id="inputPassword"
placeholder="{{passwordHint}}">
placeholder="{{passwordHint}}" aria-label="Password input field">
</div>
<div class="login-button-group">
<button type="submit" class="btn btn-large p-x-2" ng-if="!loggingIn" ng-click="submit();" ng-class="{'btn-inverse': !loginForm.$valid, 'btn-primary': loginForm.$valid}">
<button type="submit" aria-label="Login button" class="btn btn-large p-x-2" ng-if="!loggingIn" ng-click="submit();" ng-class="{'btn-inverse': !loginForm.$valid, 'btn-primary': loginForm.$valid}">
Log In
</button>
<button type="submit" class="btn btn-large p-x-2 btn-inverse btn-loading" ng-if="loggingIn">

@ -3,7 +3,7 @@
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">Scenario</label>
<div class="gf-form-select-wrapper width-15">
<select class="gf-form-input" ng-model="ctrl.target.scenarioId" ng-options="v.id as v.name for v in ctrl.scenarioList" ng-change="ctrl.scenarioChanged()"></select>
<select class="gf-form-input" ng-model="ctrl.target.scenarioId" ng-options="v.id as v.name for v in ctrl.scenarioList" ng-change="ctrl.scenarioChanged()" aria-label="Scenario Select"></select>
</div>
</div>
<div class="gf-form gf-form gf-form--grow" ng-if="ctrl.scenario.stringInput">

@ -44,7 +44,7 @@
</div>
</div>
<div class="section gf-form-group">
<div class="section gf-form-group" aria-label="X-Axis section">
<h5 class="section-heading">X-Axis</h5>
<gf-form-switch class="gf-form" label="Show" label-class="width-6" checked="ctrl.panel.xaxis.show" on-change="ctrl.render()"></gf-form-switch>

@ -0,0 +1,6 @@
export const constants = {
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
chromiumRevision: '650629',
screenShotsTruthDir: './public/e2e-test/screenShots/theTruth',
screenShotsOutputDir: './public/e2e-test/screenShots/theOutput',
};

@ -0,0 +1,51 @@
import fs from 'fs';
import { PNG } from 'pngjs';
import { Page } from 'puppeteer-core';
import pixelmatch from 'pixelmatch';
import { constants } from './constants';
export const takeScreenShot = async (page: Page, fileName: string) => {
const outputFolderExists = fs.existsSync(constants.screenShotsOutputDir);
if (!outputFolderExists) {
fs.mkdirSync(constants.screenShotsOutputDir);
}
const path = `${constants.screenShotsOutputDir}/${fileName}.png`;
await page.screenshot({ path, type: 'png', fullPage: false });
};
export const compareScreenShots = async (fileName: string) =>
new Promise(resolve => {
let filesRead = 0;
const doneReading = () => {
if (++filesRead < 2) {
return;
}
expect(screenShotFromTest.width).toEqual(screenShotFromTruth.width);
expect(screenShotFromTest.height).toEqual(screenShotFromTruth.height);
const diff = new PNG({ width: screenShotFromTest.width, height: screenShotFromTruth.height });
const numDiffPixels = pixelmatch(
screenShotFromTest.data,
screenShotFromTruth.data,
diff.data,
screenShotFromTest.width,
screenShotFromTest.height,
{ threshold: 0.1 }
);
expect(numDiffPixels).toBe(0);
resolve();
};
const screenShotFromTest = fs
.createReadStream(`${constants.screenShotsOutputDir}/${fileName}.png`)
.pipe(new PNG())
.on('parsed', doneReading);
const screenShotFromTruth = fs
.createReadStream(`${constants.screenShotsTruthDir}/${fileName}.png`)
.pipe(new PNG())
.on('parsed', doneReading);
});

@ -0,0 +1,29 @@
import puppeteer, { Browser } from 'puppeteer-core';
export const launchBrowser = async (): Promise<Browser> => {
const browserFetcher = puppeteer.createBrowserFetcher();
const localRevisions = await browserFetcher.localRevisions();
if (localRevisions.length === 0) {
throw new Error('Could not launch browser because there is no local revisions.');
}
let executablePath = null;
executablePath = browserFetcher.revisionInfo(localRevisions[0]).executablePath;
const browser = await puppeteer.launch({
headless: process.env.BROWSER ? false : true,
slowMo: process.env.SLOWMO ? 100 : 0,
defaultViewport: {
width: 1920,
height: 1080,
deviceScaleFactor: 1,
isMobile: false,
hasTouch: false,
isLandscape: false,
},
args: ['--start-fullscreen'],
executablePath,
});
return browser;
};

@ -0,0 +1,22 @@
import { Page } from 'puppeteer-core';
import { constants } from './constants';
import { loginPage } from 'e2e-test/pages/start/loginPage';
export const login = async (page: Page) => {
await loginPage.init(page);
await loginPage.navigateTo();
await loginPage.pageObjects.username.enter('admin');
await loginPage.pageObjects.password.enter('admin');
await loginPage.pageObjects.submit.click();
await loginPage.waitForResponse();
};
export const ensureLoggedIn = async (page: Page) => {
await page.goto(`${constants.baseUrl}`);
if (page.url().indexOf('login') > -1) {
console.log('Redirected to login page. Logging in...');
await login(page);
}
};

@ -0,0 +1,84 @@
import { Page } from 'puppeteer-core';
export class Selector {
static fromAriaLabel = (selector: string) => {
return `[aria-label="${selector}"]`;
};
static fromSelector = (selector: string) => {
return selector;
};
}
export interface PageObjectType {
init: (page: Page) => Promise<void>;
exists: () => Promise<void>;
containsText: (text: string) => Promise<void>;
}
export interface ClickablePageObjectType extends PageObjectType {
click: () => Promise<void>;
}
export interface InputPageObjectType extends PageObjectType {
enter: (text: string) => Promise<void>;
}
export interface SelectPageObjectType extends PageObjectType {
select: (text: string) => Promise<void>;
}
export class PageObject implements PageObjectType {
protected page: Page = null;
constructor(protected selector: string) {}
init = async (page: Page): Promise<void> => {
this.page = page;
};
exists = async (): Promise<void> => {
const options = { visible: true } as any;
await expect(this.page).not.toBeNull();
await expect(this.page).toMatchElement(this.selector, options);
};
containsText = async (text: string): Promise<void> => {
const options = { visible: true, text } as any;
await expect(this.page).not.toBeNull();
await expect(this.page).toMatchElement(this.selector, options);
};
}
export class ClickablePageObject extends PageObject implements ClickablePageObjectType {
constructor(selector: string) {
super(selector);
}
click = async (): Promise<void> => {
await expect(this.page).not.toBeNull();
await expect(this.page).toClick(this.selector);
};
}
export class InputPageObject extends PageObject implements InputPageObjectType {
constructor(selector: string) {
super(selector);
}
enter = async (text: string): Promise<void> => {
await expect(this.page).not.toBeNull();
await expect(this.page).toFill(this.selector, text);
};
}
export class SelectPageObject extends PageObject implements SelectPageObjectType {
constructor(selector: string) {
super(selector);
}
select = async (text: string): Promise<void> => {
await expect(this.page).not.toBeNull();
await this.page.select(this.selector, text);
};
}

@ -0,0 +1,110 @@
import { Page } from 'puppeteer-core';
import { constants } from './constants';
import { PageObject } from './pageObjects';
export interface ExpectSelectorConfig {
selector: string;
containsText?: string;
isVisible?: boolean;
}
export interface TestPageType<T> {
init: (page: Page) => Promise<void>;
getUrl: () => Promise<string>;
getUrlWithoutBaseUrl: () => Promise<string>;
navigateTo: () => Promise<void>;
expectSelector: (config: ExpectSelectorConfig) => Promise<void>;
waitForResponse: () => Promise<void>;
waitForNavigation: () => Promise<void>;
waitFor: (milliseconds: number) => Promise<void>;
pageObjects: PageObjects<T>;
}
type PageObjects<T> = { [P in keyof T]: T[P] };
export interface TestPageConfig<T> {
url?: string;
pageObjects?: PageObjects<T>;
}
export class TestPage<T> implements TestPageType<T> {
pageObjects: PageObjects<T> = null;
private page: Page = null;
private pageUrl: string = null;
constructor(config: TestPageConfig<T>) {
if (config.url) {
this.pageUrl = `${constants.baseUrl}${config.url}`;
}
if (config.pageObjects) {
this.pageObjects = config.pageObjects;
}
}
init = async (page: Page): Promise<void> => {
this.page = page;
if (!this.pageObjects) {
return;
}
Object.keys(this.pageObjects).forEach(key => {
const pageObject: PageObject = this.pageObjects[key];
pageObject.init(page);
});
};
navigateTo = async (): Promise<void> => {
this.throwIfNotInitialized();
await this.page.goto(this.pageUrl);
};
expectSelector = async (config: ExpectSelectorConfig): Promise<void> => {
this.throwIfNotInitialized();
const { selector, containsText, isVisible } = config;
const visible = isVisible || true;
const text = containsText;
const options = { visible, text } as any;
await expect(this.page).toMatchElement(selector, options);
};
waitForResponse = async (): Promise<void> => {
this.throwIfNotInitialized();
await this.page.waitForResponse(response => response.url() === this.pageUrl && response.status() === 200);
};
waitForNavigation = async (): Promise<void> => {
this.throwIfNotInitialized();
await this.page.waitForNavigation();
};
getUrl = async (): Promise<string> => {
this.throwIfNotInitialized();
return await this.page.url();
};
getUrlWithoutBaseUrl = async (): Promise<string> => {
this.throwIfNotInitialized();
const url = await this.getUrl();
return url.replace(constants.baseUrl, '');
};
waitFor = async (milliseconds: number) => {
this.throwIfNotInitialized();
await this.page.waitFor(milliseconds);
};
private throwIfNotInitialized = () => {
if (!this.page) {
throw new Error('pageFactory has not been initilized, did you forget to call init with a page?');
}
};
}

@ -0,0 +1,30 @@
import { Browser, Page } from 'puppeteer-core';
import { launchBrowser } from './launcher';
import { ensureLoggedIn } from './login';
export const e2eScenario = (
title: string,
testDescription: string,
callback: (browser: Browser, page: Page) => void
) => {
describe(title, () => {
let browser: Browser = null;
let page: Page = null;
beforeAll(async () => {
browser = await launchBrowser();
page = await browser.newPage();
await ensureLoggedIn(page);
});
afterAll(async () => {
if (browser) {
await browser.close();
}
});
it(testDescription, async () => {
await callback(browser, page);
});
});
};

@ -0,0 +1,22 @@
import puppeteer from 'puppeteer-core';
import { constants } from 'e2e-test/core/constants';
export const downloadBrowserIfNeeded = async (): Promise<void> => {
const browserFetcher = puppeteer.createBrowserFetcher();
const localRevisions = await browserFetcher.localRevisions();
if (localRevisions && localRevisions.length > 0) {
console.log('Found a local revision for browser, exiting install.');
return;
}
console.log('Did not find any local revisions for browser, downloading latest this might take a while.');
await browserFetcher.download(constants.chromiumRevision, (downloaded, total) => {
console.log(`Downloaded ${downloaded}bytes of ${total}bytes.`);
});
};
beforeAll(async () => {
console.log('Checking Chromium');
jest.setTimeout(60 * 1000);
await downloadBrowserIfNeeded();
});

@ -0,0 +1,13 @@
import { ClickablePageObjectType, ClickablePageObject, Selector } from 'e2e-test/core/pageObjects';
import { TestPage } from 'e2e-test/core/pages';
export interface CreateDashboardPage {
addQuery: ClickablePageObjectType;
}
export const createDashboardPage = new TestPage<CreateDashboardPage>({
url: '/dashboard/new',
pageObjects: {
addQuery: new ClickablePageObject(Selector.fromAriaLabel('Add Query CTA button')),
},
});

@ -0,0 +1,14 @@
import { ClickablePageObjectType, ClickablePageObject, Selector } from 'e2e-test/core/pageObjects';
import { TestPage } from 'e2e-test/core/pages';
export interface DashboardsPage {
dashboard: ClickablePageObjectType;
}
export const dashboardsPageFactory = (dashboardTitle: string) =>
new TestPage<DashboardsPage>({
url: '/dashboards',
pageObjects: {
dashboard: new ClickablePageObject(Selector.fromAriaLabel(dashboardTitle)),
},
});

@ -0,0 +1,20 @@
import {
ClickablePageObjectType,
ClickablePageObject,
Selector,
InputPageObjectType,
InputPageObject,
} from 'e2e-test/core/pageObjects';
import { TestPage } from 'e2e-test/core/pages';
export interface SaveDashboardModal {
name: InputPageObjectType;
save: ClickablePageObjectType;
}
export const saveDashboardModal = new TestPage<SaveDashboardModal>({
pageObjects: {
name: new InputPageObject(Selector.fromAriaLabel('Save dashboard title field')),
save: new ClickablePageObject(Selector.fromAriaLabel('Save dashboard button')),
},
});

@ -0,0 +1,13 @@
import { ClickablePageObject, Selector, ClickablePageObjectType } from 'e2e-test/core/pageObjects';
import { TestPage } from 'e2e-test/core/pages';
export interface AddDataSourcePage {
testDataDB: ClickablePageObjectType;
}
export const addDataSourcePage = new TestPage<AddDataSourcePage>({
url: '/datasources/new',
pageObjects: {
testDataDB: new ClickablePageObject(Selector.fromAriaLabel('TestData DB datasource plugin')),
},
});

@ -0,0 +1,7 @@
import { TestPage } from 'e2e-test/core/pages';
export interface DataSourcesPage {}
export const dataSourcesPage = new TestPage<DataSourcesPage>({
url: '/datasources',
});

@ -0,0 +1,22 @@
import {
ClickablePageObjectType,
PageObjectType,
ClickablePageObject,
PageObject,
Selector,
} from 'e2e-test/core/pageObjects';
import { TestPage } from 'e2e-test/core/pages';
export interface EditDataSourcePage {
saveAndTest: ClickablePageObjectType;
alert: PageObjectType;
alertMessage: PageObjectType;
}
export const editDataSourcePage = new TestPage<EditDataSourcePage>({
pageObjects: {
saveAndTest: new ClickablePageObject(Selector.fromAriaLabel('Save and Test button')),
alert: new PageObject(Selector.fromAriaLabel('Datasource settings page Alert')),
alertMessage: new PageObject(Selector.fromAriaLabel('Datasource settings page Alert message')),
},
});

@ -0,0 +1,26 @@
import {
SelectPageObjectType,
SelectPageObject,
Selector,
ClickablePageObjectType,
ClickablePageObject,
} from 'e2e-test/core/pageObjects';
import { TestPage } from 'e2e-test/core/pages';
export interface EditPanelPage {
queriesTab: ClickablePageObjectType;
saveDashboard: ClickablePageObjectType;
scenarioSelect: SelectPageObjectType;
showXAxis: ClickablePageObjectType;
visualizationTab: ClickablePageObjectType;
}
export const editPanelPage = new TestPage<EditPanelPage>({
pageObjects: {
queriesTab: new ClickablePageObject(Selector.fromAriaLabel('Queries tab button')),
saveDashboard: new ClickablePageObject(Selector.fromAriaLabel('Save dashboard navbar button')),
scenarioSelect: new SelectPageObject(Selector.fromAriaLabel('Scenario Select')),
showXAxis: new ClickablePageObject(Selector.fromSelector('[aria-label="X-Axis section"] > gf-form-switch')),
visualizationTab: new ClickablePageObject(Selector.fromAriaLabel('Visualization tab button')),
},
});

@ -0,0 +1,14 @@
import { ClickablePageObjectType, ClickablePageObject, Selector } from 'e2e-test/core/pageObjects';
import { TestPage } from 'e2e-test/core/pages';
export interface Panel {
panelTitle: ClickablePageObjectType;
share: ClickablePageObjectType;
}
export const panel = new TestPage<Panel>({
pageObjects: {
panelTitle: new ClickablePageObject(Selector.fromAriaLabel('Panel Title')),
share: new ClickablePageObject(Selector.fromAriaLabel('Share panel menu item')),
},
});

@ -0,0 +1,12 @@
import { ClickablePageObjectType, ClickablePageObject, Selector } from 'e2e-test/core/pageObjects';
import { TestPage } from 'e2e-test/core/pages';
export interface SharePanelModal {
directLinkRenderedImage: ClickablePageObjectType;
}
export const sharePanelModal = new TestPage<SharePanelModal>({
pageObjects: {
directLinkRenderedImage: new ClickablePageObject(Selector.fromAriaLabel('Link to rendered image')),
},
});

@ -0,0 +1,23 @@
import {
InputPageObject,
ClickablePageObject,
Selector,
InputPageObjectType,
ClickablePageObjectType,
} from 'e2e-test/core/pageObjects';
import { TestPage } from 'e2e-test/core/pages';
export interface LoginPage {
username: InputPageObjectType;
password: InputPageObjectType;
submit: ClickablePageObjectType;
}
export const loginPage = new TestPage<LoginPage>({
url: '/login',
pageObjects: {
username: new InputPageObject(Selector.fromAriaLabel('Username input field')),
password: new InputPageObject(Selector.fromAriaLabel('Password input field')),
submit: new ClickablePageObject(Selector.fromAriaLabel('Login button')),
},
});

@ -0,0 +1,85 @@
import { Browser, Page, Target } from 'puppeteer-core';
import { e2eScenario } from 'e2e-test/core/scenario';
import { addDataSourcePage } from 'e2e-test/pages/datasources/addDataSourcePage';
import { editDataSourcePage } from 'e2e-test/pages/datasources/editDataSourcePage';
import { dataSourcesPage } from 'e2e-test/pages/datasources/dataSources';
import { createDashboardPage } from 'e2e-test/pages/dashboards/createDashboardPage';
import { saveDashboardModal } from 'e2e-test/pages/dashboards/saveDashboardModal';
import { dashboardsPageFactory } from 'e2e-test/pages/dashboards/dashboardsPage';
import { panel } from 'e2e-test/pages/panels/panel';
import { editPanelPage } from 'e2e-test/pages/panels/editPanel';
import { constants } from 'e2e-test/core/constants';
import { sharePanelModal } from 'e2e-test/pages/panels/sharePanelModal';
import { takeScreenShot, compareScreenShots } from 'e2e-test/core/images';
e2eScenario(
'Login scenario, create test data source, dashboard, panel, and export scenario',
'should pass',
async (browser: Browser, page: Page) => {
// Add TestData DB
await addDataSourcePage.init(page);
await addDataSourcePage.navigateTo();
await addDataSourcePage.pageObjects.testDataDB.exists();
await addDataSourcePage.pageObjects.testDataDB.click();
await editDataSourcePage.init(page);
await editDataSourcePage.waitForNavigation();
await editDataSourcePage.pageObjects.saveAndTest.click();
await editDataSourcePage.pageObjects.alert.exists();
await editDataSourcePage.pageObjects.alertMessage.containsText('Data source is working');
// Verify that data source is listed
const url = await editDataSourcePage.getUrlWithoutBaseUrl();
const expectedUrl = url.substring(1, url.length - 1);
const selector = `a[href="${expectedUrl}"]`;
await dataSourcesPage.init(page);
await dataSourcesPage.navigateTo();
await dataSourcesPage.expectSelector({ selector });
// Create a new Dashboard
await createDashboardPage.init(page);
await createDashboardPage.navigateTo();
await createDashboardPage.pageObjects.addQuery.click();
await editPanelPage.init(page);
await editPanelPage.waitForNavigation();
await editPanelPage.pageObjects.queriesTab.click();
await editPanelPage.pageObjects.scenarioSelect.select('string:csv_metric_values');
await editPanelPage.pageObjects.visualizationTab.click();
await editPanelPage.pageObjects.showXAxis.click();
await editPanelPage.pageObjects.saveDashboard.click();
// Confirm save modal
await saveDashboardModal.init(page);
await saveDashboardModal.expectSelector({ selector: 'save-dashboard-as-modal' });
const dashboardTitle = new Date().toISOString();
await saveDashboardModal.pageObjects.name.enter(dashboardTitle);
await saveDashboardModal.pageObjects.save.click();
// Share the dashboard
const dashboardsPage = dashboardsPageFactory(dashboardTitle);
await dashboardsPage.init(page);
await dashboardsPage.navigateTo();
await dashboardsPage.pageObjects.dashboard.exists();
await dashboardsPage.pageObjects.dashboard.click();
await panel.init(page);
await panel.pageObjects.panelTitle.click();
await panel.pageObjects.share.click();
// Verify that a new tab is opened
const targetPromise = new Promise(resolve => browser.once('targetcreated', resolve));
await sharePanelModal.init(page);
await sharePanelModal.pageObjects.directLinkRenderedImage.click();
const newTarget: Target = (await targetPromise) as Target;
expect(newTarget.url()).toContain(`${constants.baseUrl}/render/d-solo`);
// Take snapshot of page
const newPage = await newTarget.page();
const fileName = 'smoke-test-scenario';
await takeScreenShot(newPage, fileName);
await compareScreenShots(fileName);
}
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

@ -34,5 +34,11 @@
},
"skipLibCheck": true
},
"include": ["public/app/**/*.ts", "public/app/**/*.tsx", "public/test/**/*.ts", "public/vendor/**/*.ts"]
"include": [
"public/app/**/*.ts",
"public/app/**/*.tsx",
"public/test/**/*.ts",
"public/vendor/**/*.ts",
"public/e2e-test/**/*.ts"
]
}

@ -2197,6 +2197,14 @@
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
"@types/expect-puppeteer@3.3.1":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/expect-puppeteer/-/expect-puppeteer-3.3.1.tgz#46e5944bf425b86ea13a563c7c8b86901414988d"
integrity sha512-3raSnf28NelDtv0ksvQPZs410taJZ4d70vA8sVzmbRPV04fpmQm9/BOxUCloETD/ZI1EXRpv0pzOQKhPTbm4jg==
dependencies:
"@types/jest" "*"
"@types/puppeteer" "*"
"@types/geojson@*":
version "7946.0.7"
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad"
@ -2244,6 +2252,13 @@
resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89"
integrity sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==
"@types/jest@*":
version "24.0.12"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.12.tgz#0553dd0a5ac744e7dc4e8700da6d3baedbde3e8f"
integrity sha512-60sjqMhat7i7XntZckcSGV8iREJyXXI6yFHZkSZvCPUeOnEJ/VP1rU/WpEWQ56mvoh8NhC+sfKAuJRTyGtCOow==
dependencies:
"@types/jest-diff" "*"
"@types/jest@23.3.14":
version "23.3.14"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.14.tgz#37daaf78069e7948520474c87b80092ea912520a"
@ -2298,6 +2313,20 @@
dependencies:
"@types/node" "*"
"@types/pixelmatch@4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/pixelmatch/-/pixelmatch-4.0.0.tgz#7b017c6c85e96715337f46eafbabc5a44b177530"
integrity sha512-pOF+6b0UbePCuPv1BS2k1IEeTk8ae8mhNiHms05s5WM+xV47g8Fb7KQcMn1fkJ9ccbs2IDpgPv+fGmHHvHHnrA==
dependencies:
"@types/node" "*"
"@types/pngjs@3.3.2":
version "3.3.2"
resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-3.3.2.tgz#8ed3bd655ab3a92ea32ada7a21f618e63b93b1d4"
integrity sha512-/SBsv93rVnjByzcau24rBwb+N7BHFp2LateaXz1e7m7M0Wzck/ymXTNdWVrCtkuMbwTHAnfdc3X/I/5szsTEAA==
dependencies:
"@types/node" "*"
"@types/pretty-format@20.0.1":
version "20.0.1"
resolved "https://registry.yarnpkg.com/@types/pretty-format/-/pretty-format-20.0.1.tgz#7ce03b403887b087701a2b4534464f48ce7b2f48"
@ -2308,6 +2337,20 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.1.tgz#f1a11e7babb0c3cad68100be381d1e064c68f1f6"
integrity sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg==
"@types/puppeteer-core@1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@types/puppeteer-core/-/puppeteer-core-1.9.0.tgz#5ceb397e3ff769081fb07d71289b5009392d24d3"
integrity sha512-YJwGTq0a8xZxN7/QDeW59XMdKTRNzDTc8ZVBPDB6J13GgXn1+QzgMA8pAq1/bj2FD0R7xj3nYoZra10b0HLzFw==
dependencies:
"@types/puppeteer" "*"
"@types/puppeteer@*":
version "1.12.3"
resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-1.12.3.tgz#1309882d368ed21004dfc4520864fdafcf126277"
integrity sha512-mJtUPdXqB8THRwiHPbx8pkGYi+8IPf3dMuwJS9hHpr59BwkuLDkkEJ4qMST0k6TbOUXp+wyMJii30ouSkoEtaw==
dependencies:
"@types/node" "*"
"@types/q@^1.5.1":
version "1.5.2"
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
@ -7148,6 +7191,11 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
dependencies:
homedir-polyfill "^1.0.1"
expect-puppeteer@4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/expect-puppeteer/-/expect-puppeteer-4.1.1.tgz#cda2ab7b6fa27ac24eba273bbb0296a0de538e6d"
integrity sha512-xNpu6uYJL9Qrrp4Z31MOpDWK68zAi+2qg5aMQlyOTVZNy7cAgBZiPvKCN0C1JmP3jgPZfcxhetVjZLaw/KcJOQ==
expect.js@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/expect.js/-/expect.js-0.2.0.tgz#1028533d2c1c363f74a6796ff57ec0520ded2be1"
@ -7256,7 +7304,7 @@ extglob@^2.0.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
extract-zip@^1.6.5:
extract-zip@^1.6.5, extract-zip@^1.6.6:
version "1.6.7"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9"
integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=
@ -11418,6 +11466,11 @@ mocha@4.1.0:
mkdirp "0.5.1"
supports-color "4.4.0"
module-alias@2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.0.tgz#a2e32275381642252bf0c51405f7a09a367479b5"
integrity sha512-O4bbvlZkHj2LUQhieQWWCr486ddc8X+WwRqi3QGnFKfknaxdHTOB7+xRgeyWHc6arpjgtT5SLLMMTFwUM3/x5w==
moment@2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
@ -12816,6 +12869,13 @@ pirates@^4.0.1:
dependencies:
node-modules-regexp "^1.0.0"
pixelmatch@4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-4.0.2.tgz#8f47dcec5011b477b67db03c243bc1f3085e8854"
integrity sha1-j0fc7FARtHe2fbA8JDvB8wheiFQ=
dependencies:
pngjs "^3.0.0"
pkg-dir@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
@ -12854,6 +12914,11 @@ pn@^1.1.0:
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
pngjs@3.4.0, pngjs@^3.0.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
polished@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/polished/-/polished-2.3.3.tgz#bdbaba962ba8271b0e11aa287f2befd4c87be99a"
@ -13476,6 +13541,11 @@ progress@^1.1.8:
resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=
progress@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
promise-inflight@^1.0.1, promise-inflight@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
@ -13584,6 +13654,11 @@ proxy-addr@~2.0.4:
forwarded "~0.1.2"
ipaddr.js "1.9.0"
proxy-from-env@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=
prr@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
@ -13664,6 +13739,20 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
puppeteer-core@1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-1.15.0.tgz#c8ccf246493349e5d898041f205fbeec4ed845ab"
integrity sha512-AH82x8Tx0/JkubeF6U12y8SuVB5vFgsw8lt/Ox5MhXaAktREFiotCTq324U2nPtJUnh2A8yJciDnzAmhbHidqQ==
dependencies:
debug "^4.1.0"
extract-zip "^1.6.6"
https-proxy-agent "^2.2.1"
mime "^2.0.3"
progress "^2.0.1"
proxy-from-env "^1.0.0"
rimraf "^2.6.1"
ws "^6.1.0"
q@^1.1.2:
version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
@ -17850,7 +17939,7 @@ ws@^5.2.0:
dependencies:
async-limiter "~1.0.0"
ws@^6.0.0:
ws@^6.0.0, ws@^6.1.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==

Loading…
Cancel
Save