mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
@@ -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
2
.gitignore
vendored
@@ -84,3 +84,5 @@ debug.test
|
||||
/packages/**/dist
|
||||
/packages/**/compiled
|
||||
/packages/**/.rpt2_cache
|
||||
|
||||
theOutput/
|
35
README.md
35
README.md
@@ -1,5 +1,5 @@
|
||||
[Grafana](https://grafana.com) [](https://circleci.com/gh/grafana/grafana) [](https://goreportcard.com/report/github.com/grafana/grafana) [](https://codecov.io/gh/grafana/grafana)
|
||||
================
|
||||
# [Grafana](https://grafana.com) [](https://circleci.com/gh/grafana/grafana) [](https://goreportcard.com/report/github.com/grafana/grafana) [](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).
|
||||
|
||||
|
15
jest.config.e2e.js
Normal file
15
jest.config.e2e.js
Normal file
@@ -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'],
|
||||
};
|
13
package.json
13
package.json
@@ -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 & 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>
|
||||
|
||||
|
6
public/e2e-test/core/constants.ts
Normal file
6
public/e2e-test/core/constants.ts
Normal file
@@ -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',
|
||||
};
|
51
public/e2e-test/core/images.ts
Normal file
51
public/e2e-test/core/images.ts
Normal file
@@ -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);
|
||||
});
|
29
public/e2e-test/core/launcher.ts
Normal file
29
public/e2e-test/core/launcher.ts
Normal file
@@ -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;
|
||||
};
|
22
public/e2e-test/core/login.ts
Normal file
22
public/e2e-test/core/login.ts
Normal file
@@ -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);
|
||||
}
|
||||
};
|
84
public/e2e-test/core/pageObjects.ts
Normal file
84
public/e2e-test/core/pageObjects.ts
Normal file
@@ -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);
|
||||
};
|
||||
}
|
110
public/e2e-test/core/pages.ts
Normal file
110
public/e2e-test/core/pages.ts
Normal file
@@ -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?');
|
||||
}
|
||||
};
|
||||
}
|
30
public/e2e-test/core/scenario.ts
Normal file
30
public/e2e-test/core/scenario.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
};
|
22
public/e2e-test/install/install.ts
Normal file
22
public/e2e-test/install/install.ts
Normal file
@@ -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();
|
||||
});
|
13
public/e2e-test/pages/dashboards/createDashboardPage.ts
Normal file
13
public/e2e-test/pages/dashboards/createDashboardPage.ts
Normal file
@@ -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')),
|
||||
},
|
||||
});
|
14
public/e2e-test/pages/dashboards/dashboardsPage.ts
Normal file
14
public/e2e-test/pages/dashboards/dashboardsPage.ts
Normal file
@@ -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)),
|
||||
},
|
||||
});
|
20
public/e2e-test/pages/dashboards/saveDashboardModal.ts
Normal file
20
public/e2e-test/pages/dashboards/saveDashboardModal.ts
Normal file
@@ -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')),
|
||||
},
|
||||
});
|
13
public/e2e-test/pages/datasources/addDataSourcePage.ts
Normal file
13
public/e2e-test/pages/datasources/addDataSourcePage.ts
Normal file
@@ -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')),
|
||||
},
|
||||
});
|
7
public/e2e-test/pages/datasources/dataSources.ts
Normal file
7
public/e2e-test/pages/datasources/dataSources.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { TestPage } from 'e2e-test/core/pages';
|
||||
|
||||
export interface DataSourcesPage {}
|
||||
|
||||
export const dataSourcesPage = new TestPage<DataSourcesPage>({
|
||||
url: '/datasources',
|
||||
});
|
22
public/e2e-test/pages/datasources/editDataSourcePage.ts
Normal file
22
public/e2e-test/pages/datasources/editDataSourcePage.ts
Normal file
@@ -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')),
|
||||
},
|
||||
});
|
26
public/e2e-test/pages/panels/editPanel.ts
Normal file
26
public/e2e-test/pages/panels/editPanel.ts
Normal file
@@ -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')),
|
||||
},
|
||||
});
|
14
public/e2e-test/pages/panels/panel.ts
Normal file
14
public/e2e-test/pages/panels/panel.ts
Normal file
@@ -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')),
|
||||
},
|
||||
});
|
12
public/e2e-test/pages/panels/sharePanelModal.ts
Normal file
12
public/e2e-test/pages/panels/sharePanelModal.ts
Normal file
@@ -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')),
|
||||
},
|
||||
});
|
23
public/e2e-test/pages/start/loginPage.ts
Normal file
23
public/e2e-test/pages/start/loginPage.ts
Normal file
@@ -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')),
|
||||
},
|
||||
});
|
85
public/e2e-test/scenarios/smoke.test.ts
Normal file
85
public/e2e-test/scenarios/smoke.test.ts
Normal file
@@ -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);
|
||||
}
|
||||
);
|
BIN
public/e2e-test/screenShots/theTruth/smoke-test-scenario.png
Normal file
BIN
public/e2e-test/screenShots/theTruth/smoke-test-scenario.png
Normal file
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"
|
||||
]
|
||||
}
|
||||
|
93
yarn.lock
93
yarn.lock
@@ -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==
|
||||
|
Reference in New Issue
Block a user