mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
e2e: Uses Cypress instead of Puppeteer (#20753)
* WIP: intial commit * Tests: Runs e2e tests * Refactor: Adds BASE_URL support * Refactor: Adds namespacing * Refactor: Cleans up the Page api * Build: Adds to build-branches-and-prs job for testing * Build: Hardcoded image for now * Refactor: Uses Selectors in App * Refactor: Adds addDataSource flow * WIP * Refactor: Adds e2eScenario * Refactor: Adds add and delete scenarios * Refactor: Adds logging * Refactor: Adds ability to for Selectors with variables * Refactor: Using variable selectors instead * Refactor: Adds flow until Share Panel * Refactor: Adds clicking on rendered image link * Refactor: Deletes log output * Refactor: Updates snapshots * Chore: Reverts changes * Refactor: Removes log plugin because maybe it breaks yarn build * Refactor: Adds rendered image download * Refactor: Adds image comparison * Refactor: Removes uncaught errors override * Refactor: Changes order of images to compare * Refactor: Updates truth image * Build: Updates path to CI artifacts * Refactor: Cleaning up types and config * wip * Refactor: Cleans up external api * Refactor: More cleanup * Refactor: More cleanup * Refactor: Removes usages of Pages and Flows * Refactor: Removes last traces of Cypress in spec * Refactor: Adds comments
This commit is contained in:
parent
10d36b282b
commit
58cffde0f2
@ -128,10 +128,10 @@ jobs:
|
||||
command: 'env BASE_URL=http://127.0.0.1:3000 yarn e2e-tests'
|
||||
no_output_timeout: 5m
|
||||
- store_artifacts:
|
||||
path: public/e2e-test/screenShots/theTruth
|
||||
path: public/e2e-tests/screenShots/theTruth
|
||||
destination: expected-screenshots
|
||||
- store_artifacts:
|
||||
path: public/e2e-test/screenShots/theOutput
|
||||
path: public/e2e-tests/screenShots/theOutput
|
||||
destination: output-screenshots
|
||||
- run:
|
||||
name: ci job failed
|
||||
@ -167,10 +167,10 @@ jobs:
|
||||
command: 'env BASE_URL=http://127.0.0.1:3000 yarn e2e-tests'
|
||||
no_output_timeout: 5m
|
||||
- store_artifacts:
|
||||
path: public/e2e-test/screenShots/theTruth
|
||||
path: public/e2e-tests/screenShots/theTruth
|
||||
destination: expected-screenshots
|
||||
- store_artifacts:
|
||||
path: public/e2e-test/screenShots/theOutput
|
||||
path: public/e2e-tests/screenShots/theOutput
|
||||
destination: output-screenshots
|
||||
- run:
|
||||
name: ci job failed
|
||||
|
10
.gitignore
vendored
10
.gitignore
vendored
@ -90,10 +90,16 @@ debug.test
|
||||
/packages/**/compiled
|
||||
/packages/**/.rpt2_cache
|
||||
|
||||
theOutput/
|
||||
|
||||
# Ignore go local build dependencies
|
||||
/scripts/go/bin/**
|
||||
|
||||
# Ignore compilation stats from `yarn stats`
|
||||
compilation-stats.json
|
||||
|
||||
# e2e tests
|
||||
/packages/grafana-e2e/cypress/screenshots
|
||||
/packages/grafana-e2e/cypress/videos
|
||||
/packages/grafana-e2e/cypress/logs
|
||||
/public/e2e-test/screenShots/theOutput
|
||||
/public/e2e-tests/screenShots/theOutput/*.png
|
||||
|
||||
|
@ -106,10 +106,10 @@ By default, the end-to-end tests assumes Grafana is available on `localhost:3000
|
||||
BASE_URL=http://localhost:3333 yarn e2e-tests
|
||||
```
|
||||
|
||||
To follow the tests in the browser while they're running, add the `BROWSER` and `SLOWMO` environment variables:
|
||||
To follow the tests in the browser while they're running, use the `yarn e2e-tests:debug` instead.
|
||||
|
||||
```
|
||||
BROWSER=1 SLOWMO=1 yarn e2e-tests
|
||||
yarn e2e-tests:debug
|
||||
```
|
||||
|
||||
## Configure Grafana for development
|
||||
|
@ -160,7 +160,9 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheckPackages": "yarn workspaces run typecheck",
|
||||
"jest": "jest --notify --watch",
|
||||
"e2e-tests": "jest --runInBand --config=jest.config.e2e.js",
|
||||
"e2e": "cd packages/grafana-e2e && yarn start --env BASE_URL=$BASE_URL,CIRCLE_SHA1=$CIRCLE_SHA1,SLOWMO=$SLOWMO --config integrationFolder=../../public/e2e-tests/integration,screenshotsFolder=../../public/e2e-tests/screenShots,fileServerFolder=./cypress,video=false,viewportWidth=1920,viewportHeight=1080,trashAssetsBeforeRuns=false",
|
||||
"e2e-tests": "yarn e2e",
|
||||
"e2e-tests:debug": "SLOWMO=1 yarn e2e --headed --no-exit",
|
||||
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
|
||||
"storybook": "cd packages/grafana-ui && yarn storybook --ci",
|
||||
"storybook:build": "cd packages/grafana-ui && yarn storybook:build",
|
||||
|
2
packages/grafana-e2e/CHANGELOG.md
Normal file
2
packages/grafana-e2e/CHANGELOG.md
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
|
5
packages/grafana-e2e/README.md
Normal file
5
packages/grafana-e2e/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Grafana End to End Test library
|
||||
|
||||
> **@grafana/e2e is currently in ALPHA**. Core API is unstable and can be a subject of breaking changes!
|
||||
|
||||
This package allows to run e2e tests
|
3
packages/grafana-e2e/cypress.json
Normal file
3
packages/grafana-e2e/cypress.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"supportFile": "cypress/support/index.ts"
|
||||
}
|
26
packages/grafana-e2e/cypress/plugins/cy-compare-images.js
Normal file
26
packages/grafana-e2e/cypress/plugins/cy-compare-images.js
Normal file
@ -0,0 +1,26 @@
|
||||
const fs = require('fs');
|
||||
const BlinkDiff = require('blink-diff');
|
||||
|
||||
function compareSnapshotsPlugin(args) {
|
||||
args.threshold = args.threshold || 0.001;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const diff = new BlinkDiff({
|
||||
imageAPath: args.pathToFileA,
|
||||
imageBPath: args.pathToFileB,
|
||||
thresholdType: BlinkDiff.THRESHOLD_PERCENT,
|
||||
threshold: args.threshold,
|
||||
imageOutputPath: args.pathToFileA.replace('.png', '.diff.png'),
|
||||
});
|
||||
|
||||
diff.run((error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = compareSnapshotsPlugin;
|
26
packages/grafana-e2e/cypress/plugins/cy-ts-preprocessor.js
Normal file
26
packages/grafana-e2e/cypress/plugins/cy-ts-preprocessor.js
Normal file
@ -0,0 +1,26 @@
|
||||
const wp = require('@cypress/webpack-preprocessor');
|
||||
|
||||
const webpackOptions = {
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
exclude: [/node_modules/],
|
||||
use: [
|
||||
{
|
||||
loader: 'ts-loader'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
};
|
||||
|
||||
const options = {
|
||||
webpackOptions
|
||||
};
|
||||
|
||||
module.exports = wp(options);
|
15
packages/grafana-e2e/cypress/plugins/index.js
Normal file
15
packages/grafana-e2e/cypress/plugins/index.js
Normal file
@ -0,0 +1,15 @@
|
||||
const cypressTypeScriptPreprocessor = require('./cy-ts-preprocessor');
|
||||
const compareSnapshotsPlugin = require('./cy-compare-images');
|
||||
|
||||
module.exports = on => {
|
||||
// yarn build fails with:
|
||||
// >> /Users/hugo/go/src/github.com/grafana/grafana/node_modules/stringmap/stringmap.js:99
|
||||
// >> throw new Error("StringMap expected string key");
|
||||
// on('task', {
|
||||
// failed: require('cypress-failed-log/src/failed')(),
|
||||
// });
|
||||
on('file:preprocessor', cypressTypeScriptPreprocessor);
|
||||
on('task', {
|
||||
compareSnapshotsPlugin
|
||||
});
|
||||
};
|
23
packages/grafana-e2e/cypress/support/commands.ts
Normal file
23
packages/grafana-e2e/cypress/support/commands.ts
Normal file
@ -0,0 +1,23 @@
|
||||
interface CompareSnapshotArgs {
|
||||
pathToFileA: string;
|
||||
pathToFileB: string;
|
||||
threshold?: number;
|
||||
}
|
||||
|
||||
Cypress.Commands.add('compareSnapshot', (args: CompareSnapshotArgs) => {
|
||||
cy.task('compareSnapshotsPlugin', args).then((results: any) => {
|
||||
if (results.code <= 1) {
|
||||
let msg = `\nThe screenshot:[${args.pathToFileA}] differs from :[${args.pathToFileB}]`;
|
||||
msg += '\n';
|
||||
msg += '\nCheck the Artifacts tab in the CircleCi build output for the actual screenshots.';
|
||||
msg += '\n';
|
||||
msg += '\n If the difference between expected and outcome is NOT acceptable then do the following:';
|
||||
msg += '\n - Check the code for changes that causes this difference, fix that and retry.';
|
||||
msg += '\n';
|
||||
msg += '\n If the difference between expected and outcome is acceptable then do the following:';
|
||||
msg += '\n - Replace the expected image with the outcome and retry.';
|
||||
msg += '\n';
|
||||
throw new Error(msg);
|
||||
}
|
||||
});
|
||||
});
|
7
packages/grafana-e2e/cypress/support/index.d.ts
vendored
Normal file
7
packages/grafana-e2e/cypress/support/index.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
compareSnapshot(args: CompareSnapshotArgs): void;
|
||||
}
|
||||
}
|
25
packages/grafana-e2e/cypress/support/index.ts
Normal file
25
packages/grafana-e2e/cypress/support/index.ts
Normal file
@ -0,0 +1,25 @@
|
||||
// yarn build fails with:
|
||||
// >> /Users/hugo/go/src/github.com/grafana/grafana/node_modules/stringmap/stringmap.js:99
|
||||
// >> throw new Error("StringMap expected string key");
|
||||
// require('cypress-failed-log');
|
||||
import './commands';
|
||||
|
||||
Cypress.Screenshot.defaults({
|
||||
screenshotOnRunFailure: false,
|
||||
});
|
||||
|
||||
const COMMAND_DELAY = 1000;
|
||||
|
||||
if (Cypress.env('SLOWMO')) {
|
||||
for (const command of ['visit', 'click', 'trigger', 'type', 'clear', 'reload', 'contains', 'then']) {
|
||||
Cypress.Commands.overwrite(command, (originalFn, ...args) => {
|
||||
const origVal = originalFn(...args);
|
||||
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(origVal);
|
||||
}, COMMAND_DELAY);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
7
packages/grafana-e2e/cypress/tsconfig.json
Normal file
7
packages/grafana-e2e/cypress/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["cypress"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
7
packages/grafana-e2e/index.js
Normal file
7
packages/grafana-e2e/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./index.production.js');
|
||||
} else {
|
||||
module.exports = require('./index.development.js');
|
||||
}
|
41
packages/grafana-e2e/package.json
Normal file
41
packages/grafana-e2e/package.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/e2e",
|
||||
"version": "6.4.0-pre",
|
||||
"description": "Grafana End to End Test Library",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
"e2e",
|
||||
"typescript"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"tslint": "tslint -c tslint.json --project tsconfig.json",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clean": "rimraf ./dist ./compiled",
|
||||
"bundle": "rollup -c rollup.config.ts",
|
||||
"build": "grafana-toolkit package:build --scope=e2e",
|
||||
"open": "cypress open",
|
||||
"start": "cypress run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/webpack-preprocessor": "4.1.1",
|
||||
"blink-diff": "1.0.13",
|
||||
"cypress": "3.6.1",
|
||||
"rollup": "1.6.0",
|
||||
"rollup-plugin-commonjs": "9.2.1",
|
||||
"rollup-plugin-node-resolve": "4.0.1",
|
||||
"rollup-plugin-sourcemaps": "0.4.2",
|
||||
"rollup-plugin-terser": "4.0.4",
|
||||
"rollup-plugin-typescript2": "0.19.3",
|
||||
"rollup-plugin-visualizer": "0.9.2",
|
||||
"ts-loader": "6.2.1",
|
||||
"typescript": "3.7.2"
|
||||
},
|
||||
"types": "src/index.ts"
|
||||
}
|
33
packages/grafana-e2e/rollup.config.ts
Normal file
33
packages/grafana-e2e/rollup.config.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
import commonjs from 'rollup-plugin-commonjs';
|
||||
import sourceMaps from 'rollup-plugin-sourcemaps';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
|
||||
const pkg = require('./package.json');
|
||||
|
||||
const libraryName = pkg.name;
|
||||
|
||||
const buildCjsPackage = ({ env }) => {
|
||||
return {
|
||||
input: `compiled/index.js`,
|
||||
output: [
|
||||
{
|
||||
file: `dist/index.${env}.js`,
|
||||
name: libraryName,
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
exports: 'named',
|
||||
globals: {},
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
commonjs({
|
||||
include: /node_modules/,
|
||||
}),
|
||||
resolve(),
|
||||
sourceMaps(),
|
||||
env === 'production' && terser(),
|
||||
],
|
||||
};
|
||||
};
|
||||
export default [buildCjsPackage({ env: 'development' }), buildCjsPackage({ env: 'production' })];
|
19
packages/grafana-e2e/src/flows/addDashboard.ts
Normal file
19
packages/grafana-e2e/src/flows/addDashboard.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { e2e } from '../index';
|
||||
import { Url } from '../support/url';
|
||||
|
||||
export const addDashboard = async (): Promise<{ dashboardTitle: string; uid: string }> => {
|
||||
e2e.pages.AddDashboard.visit();
|
||||
|
||||
const dashboardTitle = e2e.flows.saveNewDashboard();
|
||||
|
||||
return new Promise(resolve => {
|
||||
e2e()
|
||||
.url()
|
||||
.then((url: string) => {
|
||||
resolve({
|
||||
dashboardTitle,
|
||||
uid: Url.getDashboardUid(url),
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
19
packages/grafana-e2e/src/flows/addDataSource.ts
Normal file
19
packages/grafana-e2e/src/flows/addDataSource.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
export const addDataSource = (pluginName?: string): string => {
|
||||
pluginName = pluginName || 'TestData DB';
|
||||
|
||||
e2e.pages.DataSources.visit();
|
||||
e2e.pages.DataSources.addDataSource().click();
|
||||
|
||||
e2e.pages.AddDataSource.dataSourcePlugins(pluginName).click();
|
||||
|
||||
const dataSourceName = `e2e-${new Date().getTime()}`;
|
||||
e2e.pages.DataSource.name().clear();
|
||||
e2e.pages.DataSource.name().type(dataSourceName);
|
||||
e2e.pages.DataSource.saveAndTest().click();
|
||||
e2e.pages.DataSource.alert().should('exist');
|
||||
e2e.pages.DataSource.alertMessage().should('contain.text', 'Data source is working');
|
||||
|
||||
return dataSourceName;
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
export const assertSuccessNotification = () => {
|
||||
e2e()
|
||||
.get('.alert-success')
|
||||
.should('exist');
|
||||
};
|
27
packages/grafana-e2e/src/flows/deleteDashboard.ts
Normal file
27
packages/grafana-e2e/src/flows/deleteDashboard.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Url } from '../support/url';
|
||||
import { e2e } from '../index';
|
||||
|
||||
export const deleteDashboard = (dashBoardUid: string) => {
|
||||
e2e().request('DELETE', Url.fromBaseUrl(`/api/dashboards/uid/${dashBoardUid}`));
|
||||
|
||||
/* https://github.com/cypress-io/cypress/issues/2831
|
||||
Flows.openDashboard(dashboardName);
|
||||
|
||||
Pages.Dashboard.settings().click();
|
||||
|
||||
Pages.DashboardSettings.deleteDashBoard().click();
|
||||
|
||||
Pages.ConfirmModal.delete().click();
|
||||
|
||||
Flows.assertSuccessNotification();
|
||||
|
||||
Pages.Dashboards.visit();
|
||||
Pages.Dashboards.dashboards().each(item => {
|
||||
const text = item.text();
|
||||
Cypress.log({ message: [text] });
|
||||
if (text && text.indexOf(dashboardName) !== -1) {
|
||||
expect(false).equals(true, `Dashboard ${dashboardName} was found although it was deleted.`);
|
||||
}
|
||||
});
|
||||
*/
|
||||
};
|
23
packages/grafana-e2e/src/flows/deleteDataSource.ts
Normal file
23
packages/grafana-e2e/src/flows/deleteDataSource.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Url } from '../support/url';
|
||||
import { e2e } from '../index';
|
||||
|
||||
export const deleteDataSource = (dataSourceName: string) => {
|
||||
e2e().request('DELETE', Url.fromBaseUrl(`/api/datasources/name/${dataSourceName}`));
|
||||
|
||||
/* https://github.com/cypress-io/cypress/issues/2831
|
||||
Pages.DataSources.visit();
|
||||
Pages.DataSources.dataSources(dataSourceName).click();
|
||||
|
||||
Pages.DataSource.delete().click();
|
||||
|
||||
Pages.ConfirmModal.delete().click();
|
||||
|
||||
Pages.DataSources.visit();
|
||||
Pages.DataSources.dataSources().each(item => {
|
||||
const text = item.text();
|
||||
if (text && text.indexOf(dataSourceName) !== -1) {
|
||||
expect(false).equals(true, `Data source ${dataSourceName} was found although it was deleted.`);
|
||||
}
|
||||
});
|
||||
*/
|
||||
};
|
21
packages/grafana-e2e/src/flows/index.ts
Normal file
21
packages/grafana-e2e/src/flows/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { login } from './login';
|
||||
import { addDataSource } from './addDataSource';
|
||||
import { deleteDataSource } from './deleteDataSource';
|
||||
import { addDashboard } from './addDashboard';
|
||||
import { assertSuccessNotification } from './assertSuccessNotification';
|
||||
import { deleteDashboard } from './deleteDashboard';
|
||||
import { openDashboard } from './openDashboard';
|
||||
import { saveNewDashboard } from './saveNewDashboard';
|
||||
import { saveDashboard } from './saveDashboard';
|
||||
|
||||
export const Flows = {
|
||||
login,
|
||||
addDataSource,
|
||||
deleteDataSource,
|
||||
addDashboard,
|
||||
assertSuccessNotification,
|
||||
deleteDashboard,
|
||||
openDashboard,
|
||||
saveNewDashboard,
|
||||
saveDashboard,
|
||||
};
|
8
packages/grafana-e2e/src/flows/login.ts
Normal file
8
packages/grafana-e2e/src/flows/login.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
export const login = (username: string, password: string) => {
|
||||
e2e.pages.Login.visit();
|
||||
e2e.pages.Login.username().type(username);
|
||||
e2e.pages.Login.password().type(password);
|
||||
e2e.pages.Login.submit().click();
|
||||
};
|
6
packages/grafana-e2e/src/flows/openDashboard.ts
Normal file
6
packages/grafana-e2e/src/flows/openDashboard.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
export const openDashboard = (dashboardTitle: string) => {
|
||||
e2e.pages.Dashboards.visit();
|
||||
e2e.pages.Dashboards.dashboards(dashboardTitle).click();
|
||||
};
|
9
packages/grafana-e2e/src/flows/saveDashboard.ts
Normal file
9
packages/grafana-e2e/src/flows/saveDashboard.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
export const saveDashboard = () => {
|
||||
e2e.pages.Dashboard.toolbarItems('Save dashboard').click();
|
||||
|
||||
e2e.pages.SaveDashboardModal.save().click();
|
||||
|
||||
e2e.flows.assertSuccessNotification();
|
||||
};
|
14
packages/grafana-e2e/src/flows/saveNewDashboard.ts
Normal file
14
packages/grafana-e2e/src/flows/saveNewDashboard.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
export const saveNewDashboard = () => {
|
||||
e2e.pages.Dashboard.toolbarItems('Save dashboard').click();
|
||||
|
||||
const dashboardTitle = `e2e-${new Date().getTime()}`;
|
||||
e2e.pages.SaveDashboardAsModal.newName().clear();
|
||||
e2e.pages.SaveDashboardAsModal.newName().type(dashboardTitle);
|
||||
e2e.pages.SaveDashboardAsModal.save().click();
|
||||
|
||||
e2e.flows.assertSuccessNotification();
|
||||
|
||||
return dashboardTitle;
|
||||
};
|
3
packages/grafana-e2e/src/index.ts
Normal file
3
packages/grafana-e2e/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { e2e } from './noTypeCheck';
|
||||
|
||||
export { e2e };
|
26
packages/grafana-e2e/src/noTypeCheck.ts
Normal file
26
packages/grafana-e2e/src/noTypeCheck.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// @ts-nocheck
|
||||
// importing the e2e package in Grafana will cause transpile errors because
|
||||
// Cypress is an unknown type. Adding the Cypress types would overwrite all jest test types like
|
||||
// toBe, toEqual and so forth. That's why this file is not type checked and will be so until we
|
||||
// can solve the above mentioned issue with Cypress/Jest.
|
||||
import { e2eScenario, ScenarioArguments } from './support/scenario';
|
||||
import { Pages } from './pages';
|
||||
import { Flows } from './flows';
|
||||
|
||||
export type SelectorFunction = (text?: string) => Cypress.Chainable<any>;
|
||||
export type SelectorObject<S> = {
|
||||
visit: () => Cypress.Chainable<any>;
|
||||
selectors: S;
|
||||
};
|
||||
|
||||
const e2eObject = {
|
||||
env: (args: string) => Cypress.env(args),
|
||||
config: () => Cypress.config(),
|
||||
blobToBase64String: (blob: any) => Cypress.Blob.blobToBase64String(blob),
|
||||
imgSrcToBlob: (url: string) => Cypress.Blob.imgSrcToBlob(url),
|
||||
scenario: (args: ScenarioArguments) => e2eScenario(args),
|
||||
pages: Pages,
|
||||
flows: Flows,
|
||||
};
|
||||
|
||||
export const e2e: (() => Cypress.cy) & typeof e2eObject = Object.assign(() => cy, e2eObject);
|
8
packages/grafana-e2e/src/pages/addDashboard.ts
Normal file
8
packages/grafana-e2e/src/pages/addDashboard.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const AddDashboard = pageFactory({
|
||||
url: '/dashboard/new',
|
||||
selectors: {
|
||||
ctaButtons: (text: string) => `Add Panel Widget CTA Button ${text}`,
|
||||
},
|
||||
});
|
8
packages/grafana-e2e/src/pages/addDataSource.ts
Normal file
8
packages/grafana-e2e/src/pages/addDataSource.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const AddDataSource = pageFactory({
|
||||
url: '/datasources/new',
|
||||
selectors: {
|
||||
dataSourcePlugins: (pluginName: string) => `Data source plugin item ${pluginName}`,
|
||||
},
|
||||
});
|
8
packages/grafana-e2e/src/pages/confirmModal.ts
Normal file
8
packages/grafana-e2e/src/pages/confirmModal.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const ConfirmModal = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
delete: 'Confirm Modal Danger Button',
|
||||
},
|
||||
});
|
9
packages/grafana-e2e/src/pages/dashboard.ts
Normal file
9
packages/grafana-e2e/src/pages/dashboard.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const Dashboard = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
toolbarItems: (button: string) => `Dashboard navigation bar button ${button}`,
|
||||
backArrow: 'Dashboard settings Go Back button',
|
||||
},
|
||||
});
|
11
packages/grafana-e2e/src/pages/dashboardSettings.ts
Normal file
11
packages/grafana-e2e/src/pages/dashboardSettings.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const DashboardSettings = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
deleteDashBoard: 'Dashboard settings page delete dashboard button',
|
||||
sectionItems: 'Dashboard settings section item',
|
||||
saveDashBoard: 'Dashboard settings aside actions Save button',
|
||||
saveAsDashBoard: 'Dashboard settings aside actions Save As button',
|
||||
},
|
||||
});
|
8
packages/grafana-e2e/src/pages/dashboards.ts
Normal file
8
packages/grafana-e2e/src/pages/dashboards.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const Dashboards = pageFactory({
|
||||
url: '/dashboards',
|
||||
selectors: {
|
||||
dashboards: (title: string) => `Dashboard search item ${title}`,
|
||||
},
|
||||
});
|
12
packages/grafana-e2e/src/pages/datasource.ts
Normal file
12
packages/grafana-e2e/src/pages/datasource.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const DataSource = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
name: 'Data source settings page name input field',
|
||||
delete: 'Data source settings page Delete button',
|
||||
saveAndTest: 'Data source settings page Save and Test button',
|
||||
alert: 'Data source settings page Alert',
|
||||
alertMessage: 'Data source settings page Alert message',
|
||||
},
|
||||
});
|
9
packages/grafana-e2e/src/pages/datasources.ts
Normal file
9
packages/grafana-e2e/src/pages/datasources.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const DataSources = pageFactory({
|
||||
url: '/datasources',
|
||||
selectors: {
|
||||
dataSources: (dataSourceName: string) => `Data source list item ${dataSourceName}`,
|
||||
addDataSource: () => '.page-action-bar > .btn',
|
||||
},
|
||||
});
|
8
packages/grafana-e2e/src/pages/editPanel.ts
Normal file
8
packages/grafana-e2e/src/pages/editPanel.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const EditPanel = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
tabItems: (text: string) => `Edit panel tab item ${text}`,
|
||||
},
|
||||
});
|
5
packages/grafana-e2e/src/pages/graph/index.ts
Normal file
5
packages/grafana-e2e/src/pages/graph/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { VisualizationTab } from './visualizationTab';
|
||||
|
||||
export const Graph = {
|
||||
VisualizationTab,
|
||||
};
|
8
packages/grafana-e2e/src/pages/graph/visualizationTab.ts
Normal file
8
packages/grafana-e2e/src/pages/graph/visualizationTab.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { pageFactory } from '../../support';
|
||||
|
||||
export const VisualizationTab = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
xAxisSection: 'X-Axis section',
|
||||
},
|
||||
});
|
41
packages/grafana-e2e/src/pages/index.ts
Normal file
41
packages/grafana-e2e/src/pages/index.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { Login } from './login';
|
||||
import { AddDataSource } from './addDataSource';
|
||||
import { DataSource } from './datasource';
|
||||
import { DataSources } from './datasources';
|
||||
import { ConfirmModal } from './confirmModal';
|
||||
import { AddDashboard } from './addDashboard';
|
||||
import { Dashboard } from './dashboard';
|
||||
import { SaveDashboardAsModal } from './saveDashboardAsModal';
|
||||
import { Dashboards } from './dashboards';
|
||||
import { DashboardSettings } from './dashboardSettings';
|
||||
import { EditPanel } from './editPanel';
|
||||
import { TestData } from './testdata';
|
||||
import { Graph } from './graph';
|
||||
import { SaveDashboardModal } from './saveDashboardModal';
|
||||
import { Panel } from './panel';
|
||||
import { SharePanelModal } from './sharePanelModal';
|
||||
|
||||
export const Pages = {
|
||||
Login,
|
||||
DataSource,
|
||||
DataSources,
|
||||
AddDataSource,
|
||||
ConfirmModal,
|
||||
AddDashboard,
|
||||
Dashboard,
|
||||
Dashboards,
|
||||
SaveDashboardAsModal,
|
||||
SaveDashboardModal,
|
||||
DashboardSettings,
|
||||
SharePanelModal,
|
||||
Panels: {
|
||||
Panel,
|
||||
EditPanel,
|
||||
DataSource: {
|
||||
TestData,
|
||||
},
|
||||
Visualization: {
|
||||
Graph,
|
||||
},
|
||||
},
|
||||
};
|
10
packages/grafana-e2e/src/pages/login.ts
Normal file
10
packages/grafana-e2e/src/pages/login.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const Login = pageFactory({
|
||||
url: '/login',
|
||||
selectors: {
|
||||
username: 'Username input field',
|
||||
password: 'Password input field',
|
||||
submit: 'Login button',
|
||||
},
|
||||
});
|
9
packages/grafana-e2e/src/pages/panel.ts
Normal file
9
packages/grafana-e2e/src/pages/panel.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const Panel = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
title: (title: string) => `Panel header title item ${title}`,
|
||||
headerItems: (item: string) => `Panel header item ${item}`,
|
||||
},
|
||||
});
|
9
packages/grafana-e2e/src/pages/saveDashboardAsModal.ts
Normal file
9
packages/grafana-e2e/src/pages/saveDashboardAsModal.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const SaveDashboardAsModal = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
newName: 'Save dashboard title field',
|
||||
save: 'Save dashboard button',
|
||||
},
|
||||
});
|
8
packages/grafana-e2e/src/pages/saveDashboardModal.ts
Normal file
8
packages/grafana-e2e/src/pages/saveDashboardModal.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const SaveDashboardModal = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
save: 'Dashboard settings Save Dashboard Modal Save button',
|
||||
},
|
||||
});
|
8
packages/grafana-e2e/src/pages/sharePanelModal.ts
Normal file
8
packages/grafana-e2e/src/pages/sharePanelModal.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const SharePanelModal = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
linkToRenderedImage: 'Link to rendered image',
|
||||
},
|
||||
});
|
5
packages/grafana-e2e/src/pages/testdata/index.ts
vendored
Normal file
5
packages/grafana-e2e/src/pages/testdata/index.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
import { QueryTab } from './queryTab';
|
||||
|
||||
export const TestData = {
|
||||
QueryTab,
|
||||
};
|
8
packages/grafana-e2e/src/pages/testdata/queryTab.ts
vendored
Normal file
8
packages/grafana-e2e/src/pages/testdata/queryTab.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
import { pageFactory } from '../../support';
|
||||
|
||||
export const QueryTab = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
scenarioSelect: 'Test Data Query scenario select',
|
||||
},
|
||||
});
|
5
packages/grafana-e2e/src/support/index.ts
Normal file
5
packages/grafana-e2e/src/support/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { ScenarioContext } from './scenario';
|
||||
|
||||
export { ScenarioContext };
|
||||
export * from './types';
|
||||
export * from './selector';
|
68
packages/grafana-e2e/src/support/scenario.ts
Normal file
68
packages/grafana-e2e/src/support/scenario.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
export interface ScenarioContext {
|
||||
dataSourceName?: string;
|
||||
dashboardTitle?: string;
|
||||
dashboardUid?: string;
|
||||
}
|
||||
|
||||
export interface ScenarioArguments {
|
||||
describeName: string;
|
||||
itName: string;
|
||||
scenario: (context: ScenarioContext) => void;
|
||||
skipScenario?: boolean;
|
||||
addScenarioDataSource?: boolean;
|
||||
addScenarioDashBoard?: boolean;
|
||||
}
|
||||
|
||||
export const e2eScenario = ({
|
||||
describeName,
|
||||
itName,
|
||||
scenario,
|
||||
skipScenario = false,
|
||||
addScenarioDataSource = false,
|
||||
addScenarioDashBoard = false,
|
||||
}: ScenarioArguments) => {
|
||||
describe(describeName, () => {
|
||||
if (skipScenario) {
|
||||
it.skip(itName, () => {
|
||||
// @ts-ignore yarn start in root throws error otherwise
|
||||
expect(false).equals(true);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let scenarioDataSource: string;
|
||||
let scenarioDashBoardTitle: string;
|
||||
let scenarioDashBoardUid: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
e2e.flows.login('admin', 'admin');
|
||||
if (addScenarioDataSource) {
|
||||
scenarioDataSource = e2e.flows.addDataSource('TestData DB');
|
||||
}
|
||||
if (addScenarioDashBoard) {
|
||||
const { dashboardTitle, uid } = await e2e.flows.addDashboard();
|
||||
scenarioDashBoardTitle = dashboardTitle;
|
||||
scenarioDashBoardUid = uid;
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (scenarioDataSource) {
|
||||
e2e.flows.deleteDataSource(scenarioDataSource);
|
||||
}
|
||||
if (scenarioDashBoardUid) {
|
||||
e2e.flows.deleteDashboard(scenarioDashBoardUid);
|
||||
}
|
||||
});
|
||||
|
||||
it(itName, () => {
|
||||
scenario({
|
||||
dashboardTitle: scenarioDashBoardTitle,
|
||||
dashboardUid: scenarioDashBoardUid,
|
||||
dataSourceName: scenarioDataSource,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
9
packages/grafana-e2e/src/support/selector.ts
Normal file
9
packages/grafana-e2e/src/support/selector.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface SelectorApi {
|
||||
fromAriaLabel: (selector: string) => string;
|
||||
fromSelector: (selector: string) => string;
|
||||
}
|
||||
|
||||
export const Selector: SelectorApi = {
|
||||
fromAriaLabel: (selector: string) => `[aria-label="${selector}"]`,
|
||||
fromSelector: (selector: string) => selector,
|
||||
};
|
41
packages/grafana-e2e/src/support/types.ts
Normal file
41
packages/grafana-e2e/src/support/types.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { Selector } from './selector';
|
||||
import { Url } from './url';
|
||||
import { e2e } from '../index';
|
||||
import { SelectorFunction, SelectorObject } from '../noTypeCheck';
|
||||
|
||||
export type Selectors = Record<string, string | Function>;
|
||||
export type PageObjects<S> = { [P in keyof S]: SelectorFunction };
|
||||
export type PageFactory<S> = PageObjects<S> & SelectorObject<S>;
|
||||
export interface PageFactoryArgs<S extends Selectors> {
|
||||
url?: string;
|
||||
selectors: S;
|
||||
}
|
||||
|
||||
export const pageFactory = <S extends Selectors>({ url, selectors }: PageFactoryArgs<S>): PageFactory<S> => {
|
||||
const visit = () => e2e().visit(Url.fromBaseUrl(url));
|
||||
const pageObjects: PageObjects<S> = {} as PageObjects<S>;
|
||||
const keys = Object.keys(selectors);
|
||||
|
||||
keys.forEach(key => {
|
||||
const value = selectors[key];
|
||||
if (typeof value === 'string') {
|
||||
// @ts-ignore
|
||||
pageObjects[key] = () => e2e().get(Selector.fromAriaLabel(value));
|
||||
}
|
||||
if (typeof value === 'function') {
|
||||
// @ts-ignore
|
||||
pageObjects[key] = (text?: string) => {
|
||||
if (!text) {
|
||||
return e2e().get(value());
|
||||
}
|
||||
return e2e().get(Selector.fromAriaLabel(value(text)));
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
visit,
|
||||
...pageObjects,
|
||||
selectors,
|
||||
};
|
||||
};
|
25
packages/grafana-e2e/src/support/url.ts
Normal file
25
packages/grafana-e2e/src/support/url.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
export interface UrlApi {
|
||||
fromBaseUrl: (url: string | undefined) => string;
|
||||
getDashboardUid: (url: string) => string;
|
||||
}
|
||||
|
||||
const uidRegex = '\\/d\\/(.*)\\/';
|
||||
const getBaseUrl = () => e2e.env('BASE_URL') || e2e.config().baseUrl || 'http://localhost:3000';
|
||||
|
||||
export const Url: UrlApi = {
|
||||
fromBaseUrl: (url: string | undefined) => {
|
||||
url = url || '';
|
||||
const strippedUrl = url.replace('^/', '');
|
||||
return `${getBaseUrl()}${strippedUrl}`;
|
||||
},
|
||||
getDashboardUid: (url: string) => {
|
||||
const matches = url.match(uidRegex);
|
||||
if (!matches) {
|
||||
throw new Error(`Couldn't parse uid from ${url}`);
|
||||
}
|
||||
|
||||
return matches[1];
|
||||
},
|
||||
};
|
4
packages/grafana-e2e/tsconfig.build.json
Normal file
4
packages/grafana-e2e/tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["dist", "node_modules", "**/*.test.ts", "**/*.test.tsx"]
|
||||
}
|
12
packages/grafana-e2e/tsconfig.json
Normal file
12
packages/grafana-e2e/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"types": ["cypress"],
|
||||
"rootDirs": ["."],
|
||||
"typeRoots": ["./node_modules/@types", "types"],
|
||||
"declarationDir": "dist",
|
||||
"outDir": "compiled"
|
||||
}
|
||||
}
|
4
packages/grafana-e2e/tslint.json
Normal file
4
packages/grafana-e2e/tslint.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tslint.json",
|
||||
"rules": {}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
import React, { PureComponent, SyntheticEvent, ChangeEvent } from 'react';
|
||||
import React, { ChangeEvent, PureComponent, SyntheticEvent } from 'react';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
import { FormModel } from './LoginCtrl';
|
||||
|
||||
interface Props {
|
||||
@ -72,7 +74,7 @@ export class LoginForm extends PureComponent<Props, State> {
|
||||
className="gf-form-input login-form-input"
|
||||
required
|
||||
placeholder={this.props.loginHint}
|
||||
aria-label="Username input field"
|
||||
aria-label={e2e.pages.Login.selectors.username}
|
||||
onChange={this.onChangeUsername}
|
||||
/>
|
||||
</div>
|
||||
@ -85,7 +87,7 @@ export class LoginForm extends PureComponent<Props, State> {
|
||||
ng-model="formModel.password"
|
||||
id="inputPassword"
|
||||
placeholder={this.props.passwordHint}
|
||||
aria-label="Password input field"
|
||||
aria-label={e2e.pages.Login.selectors.password}
|
||||
onChange={this.onChangePassword}
|
||||
/>
|
||||
</div>
|
||||
@ -93,7 +95,7 @@ export class LoginForm extends PureComponent<Props, State> {
|
||||
{!this.props.isLoggingIn ? (
|
||||
<button
|
||||
type="submit"
|
||||
aria-label="Login button"
|
||||
aria-label={e2e.pages.Login.selectors.submit}
|
||||
className={`btn btn-large p-x-2 ${this.state.valid ? 'btn-primary' : 'btn-inverse'}`}
|
||||
onClick={this.onSubmit}
|
||||
disabled={!this.state.valid}
|
||||
|
@ -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}}" aria-label="{{::item.title}}">
|
||||
<a ng-repeat="item in section.items" class="search-item search-item--indent" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}" aria-label={{ctrl.selectors.dashboards(item.title)}}>
|
||||
<div ng-click="ctrl.toggleSelection(item, $event)" class="center-vh">
|
||||
<gf-form-checkbox
|
||||
ng-show="ctrl.editable"
|
||||
|
@ -1,4 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
import coreModule from '../../core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { CoreEvents } from 'app/types';
|
||||
@ -9,9 +11,12 @@ export class SearchResultsCtrl {
|
||||
onTagSelected: any;
|
||||
onFolderExpanding: any;
|
||||
editable: boolean;
|
||||
selectors: typeof e2e.pages.Dashboards.selectors;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $location: any) {}
|
||||
constructor(private $location: any) {
|
||||
this.selectors = e2e.pages.Dashboards.selectors;
|
||||
}
|
||||
|
||||
toggleFolderExpand(section: any) {
|
||||
if (section.toggle) {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { CoreEvents } from 'app/types';
|
||||
@ -71,6 +73,7 @@ export class UtilSrv {
|
||||
scope.yesText = payload.yesText || 'Yes';
|
||||
scope.noText = payload.noText || 'Cancel';
|
||||
scope.confirmTextValid = scope.confirmText ? false : true;
|
||||
scope.selectors = e2e.pages.ConfirmModal.selectors;
|
||||
|
||||
appEvents.emit(CoreEvents.showModal, {
|
||||
src: 'public/app/partials/confirm_modal.html',
|
||||
|
@ -1,20 +1,17 @@
|
||||
// Libraries
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { LocationUpdate } from '@grafana/runtime';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
// Utils
|
||||
import config from 'app/core/config';
|
||||
import store from 'app/core/store';
|
||||
|
||||
// Store
|
||||
import { store as reduxStore } from 'app/store/store';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
// Types
|
||||
import { PanelModel } from '../../state';
|
||||
import { DashboardModel } from '../../state';
|
||||
import { DashboardModel, PanelModel } from '../../state';
|
||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||
import { LocationUpdate } from '@grafana/runtime';
|
||||
|
||||
export type PanelPluginInfo = { id: any; defaults: { gridPos: { w: any; h: any }; title: any } };
|
||||
|
||||
@ -141,7 +138,7 @@ export class AddPanelWidget extends React.Component<Props, State> {
|
||||
href="#"
|
||||
onClick={onClick}
|
||||
className="add-panel-widget__link btn btn-inverse"
|
||||
aria-label={`${text} CTA button`}
|
||||
aria-label={e2e.pages.AddDashboard.selectors.ctaButtons(text)}
|
||||
>
|
||||
<div className="add-panel-widget__icon">
|
||||
<i className={`gicon gicon-${icon}`} />
|
||||
|
@ -35,7 +35,7 @@ exports[`Render should render component 1`] = `
|
||||
>
|
||||
<div>
|
||||
<a
|
||||
aria-label="Add Query CTA button"
|
||||
aria-label="Add Panel Widget CTA Button Add Query"
|
||||
className="add-panel-widget__link btn btn-inverse"
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
@ -54,7 +54,7 @@ exports[`Render should render component 1`] = `
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
aria-label="Choose Visualization CTA button"
|
||||
aria-label="Add Panel Widget CTA Button Choose Visualization"
|
||||
className="add-panel-widget__link btn btn-inverse"
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Libaries
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
// Utils & Services
|
||||
import { appEvents } from 'app/core/app_events';
|
||||
import { PlaylistSrv } from 'app/features/playlist/playlist_srv';
|
||||
@ -159,7 +160,7 @@ export class DashNav extends PureComponent<Props> {
|
||||
<button
|
||||
className="navbar-edit__back-btn"
|
||||
onClick={this.onClose}
|
||||
aria-label="Dashboard settings Go Back button"
|
||||
aria-label={e2e.pages.Dashboard.selectors.backArrow}
|
||||
>
|
||||
<i className="fa fa-arrow-left" />
|
||||
</button>
|
||||
|
@ -1,8 +1,8 @@
|
||||
// Libraries
|
||||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
// Components
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
interface Props {
|
||||
icon: string;
|
||||
@ -19,7 +19,7 @@ export const DashNavButton: FunctionComponent<Props> = ({ icon, tooltip, classSu
|
||||
<button
|
||||
className={`btn navbar-button navbar-button--${classSuffix}`}
|
||||
onClick={onClick}
|
||||
aria-label={`${tooltip} navbar button`}
|
||||
aria-label={e2e.pages.Dashboard.selectors.toolbarItems(tooltip)}
|
||||
>
|
||||
<i className={icon} />
|
||||
</button>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { coreModule, appEvents, contextSrv } from 'app/core/core';
|
||||
import { appEvents, contextSrv, coreModule } from 'app/core/core';
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
@ -9,6 +9,7 @@ import { DashboardSrv } from '../../services/DashboardSrv';
|
||||
import { CoreEvents } from 'app/types';
|
||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
export class SettingsCtrl {
|
||||
dashboard: DashboardModel;
|
||||
@ -21,6 +22,7 @@ export class SettingsCtrl {
|
||||
canDelete: boolean;
|
||||
sections: any[];
|
||||
hasUnsavedFolderChange: boolean;
|
||||
selectors: typeof e2e.pages.DashboardSettings.selectors;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
@ -53,6 +55,7 @@ export class SettingsCtrl {
|
||||
this.$rootScope.onAppEvent(CoreEvents.routeUpdated, this.onRouteUpdated.bind(this), $scope);
|
||||
this.$rootScope.appEvent(CoreEvents.dashScroll, { animate: false, pos: 0 });
|
||||
this.$rootScope.onAppEvent(CoreEvents.dashboardSaved, this.onPostSave.bind(this), $scope);
|
||||
this.selectors = e2e.pages.DashboardSettings.selectors;
|
||||
}
|
||||
|
||||
buildSectionList() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<aside class="dashboard-settings__aside">
|
||||
<a href="{{::section.url}}" class="dashboard-settings__nav-item" ng-class="{active: ctrl.viewId === section.id}" ng-repeat="section in ctrl.sections" aria-label="{{'Dashboard settings section ' + section.title}}">
|
||||
<a href="{{::section.url}}" class="dashboard-settings__nav-item" ng-class="{active: ctrl.viewId === section.id}" ng-repeat="section in ctrl.sections" aria-label={{ctrl.selectors.sectionItems}}>
|
||||
<i class="{{::section.icon}}"></i>
|
||||
{{::section.title}}
|
||||
</a>
|
||||
@ -9,7 +9,7 @@
|
||||
class="btn btn-primary"
|
||||
ng-click="ctrl.saveDashboard()"
|
||||
ng-show="ctrl.canSave"
|
||||
aria-label="Dashboard settings aside actions Save button"
|
||||
aria-label={{ctrl.selectors.saveDashBoard}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
@ -17,7 +17,7 @@
|
||||
class="btn btn-inverse"
|
||||
ng-click="ctrl.openSaveAsModal()"
|
||||
ng-show="ctrl.canSaveAs"
|
||||
aria-label="Dashboard settings aside actions Save As button"
|
||||
aria-label={{ctrl.selectors.saveAsDashBoard}}
|
||||
>
|
||||
Save As...
|
||||
</button>
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { DashboardSrv } from '../../services/DashboardSrv';
|
||||
import { CloneOptions } from '../../state/DashboardModel';
|
||||
@ -57,7 +59,7 @@ const template = `
|
||||
class="btn btn-primary"
|
||||
ng-class="{'btn-primary--processing': ctrl.isSaving}"
|
||||
ng-disabled="ctrl.saveForm.$invalid || ctrl.isSaving"
|
||||
aria-label="Dashboard settings Save Dashboard Modal Save button"
|
||||
aria-label={{ctrl.selectors.save}}
|
||||
>
|
||||
<span ng-if="!ctrl.isSaving">Save</span>
|
||||
<span ng-if="ctrl.isSaving === true">Saving...</span>
|
||||
@ -82,6 +84,7 @@ export class SaveDashboardModalCtrl {
|
||||
dismiss: () => void;
|
||||
timeChange = false;
|
||||
variableValueChange = false;
|
||||
selectors: typeof e2e.pages.SaveDashboardModal.selectors;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private dashboardSrv: DashboardSrv) {
|
||||
@ -90,6 +93,7 @@ export class SaveDashboardModalCtrl {
|
||||
this.isSaving = false;
|
||||
this.timeChange = this.dashboardSrv.getCurrent().hasTimeChanged();
|
||||
this.variableValueChange = this.dashboardSrv.getCurrent().hasVariableValuesChanged();
|
||||
this.selectors = e2e.pages.SaveDashboardModal.selectors;
|
||||
}
|
||||
|
||||
save() {
|
||||
|
@ -1,6 +1,8 @@
|
||||
import angular, { ILocationService } from 'angular';
|
||||
import config from 'app/core/config';
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
import config from 'app/core/config';
|
||||
import { appendQueryToUrl, toUrlParams } from 'app/core/utils/url';
|
||||
import { TimeSrv } from '../../services/TimeSrv';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
@ -23,6 +25,7 @@ export function ShareModalCtrl(
|
||||
theme: 'current',
|
||||
};
|
||||
$scope.editor = { index: $scope.tabIndex || 0 };
|
||||
$scope.selectors = e2e.pages.SharePanelModal.selectors;
|
||||
|
||||
$scope.init = () => {
|
||||
$scope.panel = $scope.model && $scope.model.panel ? $scope.model.panel : $scope.panel; // React pass panel and dashboard in the "model" property
|
||||
|
@ -92,7 +92,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="modeSharePanel">
|
||||
<a href="{{imageUrl}}" target="_blank" aria-label="Link to rendered image"><i class="fa fa-camera"></i> Direct link rendered image</a>
|
||||
<a href="{{imageUrl}}" target="_blank" aria-label={{selectors.linkToRenderedImage}}><i class="fa fa-camera"></i> Direct link rendered image</a>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
@ -1,7 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { isEqual } from 'lodash';
|
||||
import { ScopedVars } from '@grafana/data';
|
||||
import { DataLink, ScopedVars } from '@grafana/data';
|
||||
import { ClickOutsideWrapper } from '@grafana/ui';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
import PanelHeaderCorner from './PanelHeaderCorner';
|
||||
import { PanelHeaderMenu } from './PanelHeaderMenu';
|
||||
@ -9,8 +11,6 @@ import templateSrv from 'app/features/templating/template_srv';
|
||||
|
||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||
import { ClickOutsideWrapper } from '@grafana/ui';
|
||||
import { DataLink } from '@grafana/data';
|
||||
import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||
|
||||
export interface Props {
|
||||
@ -96,7 +96,7 @@ export class PanelHeader extends Component<Props, State> {
|
||||
className="panel-title-container"
|
||||
onClick={this.onMenuToggle}
|
||||
onMouseDown={this.onMouseDown}
|
||||
aria-label="Panel Title"
|
||||
aria-label={e2e.pages.Panels.Panel.selectors.title(title)}
|
||||
>
|
||||
<div className="panel-title">
|
||||
<span className="icon-gf panel-alert-icon" />
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { FC } from 'react';
|
||||
import { PanelMenuItem } from '@grafana/data';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
interface Props {
|
||||
children: any;
|
||||
@ -14,7 +15,7 @@ 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" aria-label={`${props.text} panel menu item`}>
|
||||
<span className="dropdown-item-text" aria-label={e2e.pages.Panels.Panel.selectors.headerItems(props.text)}>
|
||||
{props.text}
|
||||
</span>
|
||||
{props.shortcut && <span className="dropdown-menu-item-shortcut">{props.shortcut}</span>}
|
||||
|
@ -5,6 +5,7 @@ import { connect } from 'react-redux';
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
import { PanelPlugin, PanelPluginMeta } from '@grafana/data';
|
||||
import { AngularComponent, config } from '@grafana/runtime';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
import { QueriesTab } from './QueriesTab';
|
||||
import VisualizationTab from './VisualizationTab';
|
||||
@ -130,7 +131,7 @@ function TabItem({ tab, activeTab, onClick }: TabItemParams) {
|
||||
|
||||
return (
|
||||
<div className="panel-editor-tabs__item" onClick={() => onClick(tab)}>
|
||||
<a className={tabClasses} aria-label={`${tab.text} tab button`}>
|
||||
<a className={tabClasses} aria-label={e2e.pages.Panels.EditPanel.selectors.tabItems(tab.text)}>
|
||||
<Tooltip content={`${tab.text}`} placement="auto">
|
||||
<i className={`gicon gicon-${tab.id}${activeTab === tab.id ? '-active' : ''}`} />
|
||||
</Tooltip>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { DataSourceSettings } from '@grafana/data';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
export interface Props {
|
||||
dataSource: DataSourceSettings;
|
||||
@ -19,7 +20,7 @@ export class DataSourcesListItem extends PureComponent<Props> {
|
||||
<img src={dataSource.typeLogoUrl} alt={dataSource.name} />
|
||||
</figure>
|
||||
<div className="card-item-details">
|
||||
<div className="card-item-name" aria-label={`Data source list item for ${dataSource.name}`}>
|
||||
<div className="card-item-name" aria-label={e2e.pages.DataSources.selectors.dataSources(dataSource.name)}>
|
||||
{dataSource.name}
|
||||
{dataSource.isDefault && <span className="btn btn-secondary btn-small card-item-label">default</span>}
|
||||
</div>
|
||||
|
@ -1,13 +1,15 @@
|
||||
import React, { PureComponent, FC } from 'react';
|
||||
import React, { FC, PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { DataSourcePluginMeta, NavModel, PluginType } from '@grafana/data';
|
||||
import { List } from '@grafana/ui';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import { StoreState } from 'app/types';
|
||||
import { addDataSource, loadDataSourceTypes, setDataSourceTypeSearchQuery } from './state/actions';
|
||||
import { getDataSourceTypes } from './state/selectors';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
import { List } from '@grafana/ui';
|
||||
import { DataSourcePluginMeta, NavModel, PluginType } from '@grafana/data';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
@ -184,7 +186,11 @@ const DataSourceTypeCard: FC<DataSourceTypeCardProps> = props => {
|
||||
const learnMoreLink = plugin.info.links && plugin.info.links.length > 0 ? plugin.info.links[0].url : null;
|
||||
|
||||
return (
|
||||
<div className="add-data-source-item" onClick={onClick} aria-label={`${plugin.name} datasource plugin`}>
|
||||
<div
|
||||
className="add-data-source-item"
|
||||
onClick={onClick}
|
||||
aria-label={e2e.pages.AddDataSource.selectors.dataSourcePlugins(plugin.name)}
|
||||
>
|
||||
<img className="add-data-source-item-logo" src={plugin.info.logos.small} />
|
||||
<div className="add-data-source-item-text-wrapper">
|
||||
<span className="add-data-source-item-text">{plugin.name}</span>
|
||||
|
@ -32,7 +32,7 @@ exports[`Render should render component 1`] = `
|
||||
className="card-item-details"
|
||||
>
|
||||
<div
|
||||
aria-label="Data source list item for gdev-cloudwatch"
|
||||
aria-label="Data source list item gdev-cloudwatch"
|
||||
className="card-item-name"
|
||||
>
|
||||
gdev-cloudwatch
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { FC } from 'react';
|
||||
import { FormLabel, Input, Switch } from '@grafana/ui';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
export interface Props {
|
||||
dataSourceName: string;
|
||||
@ -28,7 +29,7 @@ const BasicSettings: FC<Props> = ({ dataSourceName, isDefault, onDefaultChange,
|
||||
placeholder="Name"
|
||||
onChange={event => onNameChange(event.target.value)}
|
||||
required
|
||||
aria-label="Datasource settings page name input field"
|
||||
aria-label={e2e.pages.DataSource.selectors.name}
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
|
@ -1,4 +1,6 @@
|
||||
import React, { FC } from 'react';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
import config from 'app/core/config';
|
||||
|
||||
export interface Props {
|
||||
@ -17,7 +19,7 @@ const ButtonRow: FC<Props> = ({ isReadOnly, onDelete, onSubmit, onTest }) => {
|
||||
className="btn btn-primary"
|
||||
disabled={isReadOnly}
|
||||
onClick={event => onSubmit(event)}
|
||||
aria-label="Save and Test button"
|
||||
aria-label={e2e.pages.DataSource.selectors.saveAndTest}
|
||||
>
|
||||
Save & Test
|
||||
</button>
|
||||
@ -32,7 +34,7 @@ const ButtonRow: FC<Props> = ({ isReadOnly, onDelete, onSubmit, onTest }) => {
|
||||
className="btn btn-danger"
|
||||
disabled={isReadOnly}
|
||||
onClick={onDelete}
|
||||
aria-label="Delete button"
|
||||
aria-label={e2e.pages.DataSource.selectors.delete}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
|
@ -3,6 +3,7 @@ import React, { PureComponent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import isString from 'lodash/isString';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
// Components
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import { GenericDataSourcePlugin, PluginSettings } from './PluginSettings';
|
||||
@ -271,7 +272,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
|
||||
|
||||
<div className="gf-form-group">
|
||||
{testingMessage && (
|
||||
<div className={`alert-${testingStatus} alert`} aria-label="Datasource settings page Alert">
|
||||
<div className={`alert-${testingStatus} alert`} aria-label={e2e.pages.DataSource.selectors.alert}>
|
||||
<div className="alert-icon">
|
||||
{testingStatus === 'error' ? (
|
||||
<i className="fa fa-exclamation-triangle" />
|
||||
@ -280,7 +281,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
|
||||
)}
|
||||
</div>
|
||||
<div className="alert-body">
|
||||
<div className="alert-title" aria-label="Datasource settings page Alert message">
|
||||
<div className="alert-title" aria-label={e2e.pages.DataSource.selectors.alertMessage}>
|
||||
{testingMessage}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -22,7 +22,7 @@ exports[`Render should render component 1`] = `
|
||||
Name
|
||||
</Component>
|
||||
<Input
|
||||
aria-label="Datasource settings page name input field"
|
||||
aria-label="Data source settings page name input field"
|
||||
className="gf-form-input max-width-23"
|
||||
onChange={[Function]}
|
||||
placeholder="Name"
|
||||
|
@ -12,7 +12,7 @@ exports[`Render should render component 1`] = `
|
||||
Test
|
||||
</button>
|
||||
<button
|
||||
aria-label="Delete button"
|
||||
aria-label="Data source settings page Delete button"
|
||||
className="btn btn-danger"
|
||||
disabled={true}
|
||||
onClick={[MockFunction]}
|
||||
@ -34,7 +34,7 @@ exports[`Render should render with buttons enabled 1`] = `
|
||||
className="gf-form-button-row"
|
||||
>
|
||||
<button
|
||||
aria-label="Save and Test button"
|
||||
aria-label="Data source settings page Save and Test button"
|
||||
className="btn btn-primary"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
@ -43,7 +43,7 @@ exports[`Render should render with buttons enabled 1`] = `
|
||||
Save & Test
|
||||
</button>
|
||||
<button
|
||||
aria-label="Delete button"
|
||||
aria-label="Data source settings page Delete button"
|
||||
className="btn btn-danger"
|
||||
disabled={false}
|
||||
onClick={[MockFunction]}
|
||||
|
@ -6,6 +6,7 @@ import Drop from 'tether-drop';
|
||||
import baron from 'baron';
|
||||
import { PanelEvents } from '@grafana/data';
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
const module = angular.module('grafana.directives');
|
||||
|
||||
@ -21,7 +22,7 @@ const panelTemplate = `
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</span>
|
||||
|
||||
<panel-header class="panel-title-container" panel-ctrl="ctrl" aria-label="Panel Title"></panel-header>
|
||||
<panel-header class="panel-title-container" panel-ctrl="ctrl" aria-label={{ctrl.selectors.title(ctrl.panel.title)}}></panel-header>
|
||||
</div>
|
||||
|
||||
<div class="panel-content">
|
||||
@ -42,6 +43,7 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
||||
const panelContent = elem.find('.panel-content');
|
||||
const cornerInfoElem = elem.find('.panel-info-corner');
|
||||
const ctrl = scope.ctrl;
|
||||
ctrl.selectors = e2e.pages.Panels.Panel.selectors;
|
||||
let infoDrop: any;
|
||||
let panelScrollbar: any;
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { coreModule } from 'app/core/core';
|
||||
import { AngularPanelMenuItem } from '@grafana/data';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
const template = `
|
||||
<span class="panel-title">
|
||||
@ -35,7 +36,9 @@ function renderMenuItem(item: AngularPanelMenuItem, ctrl: any) {
|
||||
}
|
||||
|
||||
html += `><i class="${item.icon}"></i>`;
|
||||
html += `<span class="dropdown-item-text" aria-label="${item.text} panel menu item">${item.text}</span>`;
|
||||
html += `<span class="dropdown-item-text" aria-label="${e2e.pages.Panels.Panel.selectors.headerItems(item.text)}">${
|
||||
item.text
|
||||
}</span>`;
|
||||
|
||||
if (item.shortcut) {
|
||||
html += `<span class="dropdown-menu-item-shortcut">${item.shortcut}</span>`;
|
||||
|
@ -26,7 +26,7 @@
|
||||
|
||||
<div class="confirm-modal-buttons">
|
||||
<button ng-show="onAltAction" type="button" class="btn btn-primary" ng-click="dismiss();onAltAction();">{{altActionText}}</button>
|
||||
<button ng-show="onConfirm" type="button" class="btn btn-danger" ng-click="onConfirm();dismiss();" ng-disabled="!confirmTextValid" give-focus="true" aria-label="Confirm Modal Danger Button">{{yesText}}</button>
|
||||
<button ng-show="onConfirm" type="button" class="btn btn-danger" ng-click="onConfirm();dismiss();" ng-disabled="!confirmTextValid" give-focus="true" aria-label={{selectors.delete}}>{{yesText}}</button>
|
||||
<button type="button" class="btn btn-inverse" ng-click="dismiss()">{{noText}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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()" aria-label="Scenario Select"></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={{ctrl.selectors.scenarioSelect}}></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form gf-form--grow" ng-if="ctrl.scenario.stringInput">
|
||||
|
@ -1,9 +1,10 @@
|
||||
import _ from 'lodash';
|
||||
import { dateMath, dateTime } from '@grafana/data';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
import { QueryCtrl } from 'app/plugins/sdk';
|
||||
import { defaultQuery } from './runStreams';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { dateTime, dateMath } from '@grafana/data';
|
||||
|
||||
export const defaultPulse: any = {
|
||||
timeStep: 60,
|
||||
@ -30,6 +31,7 @@ export class TestDataQueryCtrl extends QueryCtrl {
|
||||
selectedPoint: any;
|
||||
|
||||
showLabels = false;
|
||||
selectors: typeof e2e.pages.Panels.DataSource.TestData.QueryTab.selectors;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope: any, $injector: any) {
|
||||
@ -40,6 +42,7 @@ export class TestDataQueryCtrl extends QueryCtrl {
|
||||
this.newPointTime = dateTime();
|
||||
this.selectedPoint = { text: 'Select point', value: null };
|
||||
this.showLabels = showLabelsFor.includes(this.target.scenarioId);
|
||||
this.selectors = e2e.pages.Panels.DataSource.TestData.QueryTab.selectors;
|
||||
}
|
||||
|
||||
getPoints() {
|
||||
|
@ -44,7 +44,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section gf-form-group" aria-label="X-Axis section">
|
||||
<div class="section gf-form-group" aria-label={{ctrl.selectors.xAxisSection}}>
|
||||
<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>
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { e2e } from '@grafana/e2e';
|
||||
import { GraphCtrl } from './module';
|
||||
|
||||
export class AxesEditorCtrl {
|
||||
@ -7,6 +8,7 @@ export class AxesEditorCtrl {
|
||||
xAxisModes: any;
|
||||
xAxisStatOptions: any;
|
||||
xNameSegment: any;
|
||||
selectors: typeof e2e.pages.Panels.Visualization.Graph.VisualizationTab.selectors;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope: any) {
|
||||
@ -43,6 +45,7 @@ export class AxesEditorCtrl {
|
||||
this.panel.xaxis.name = 'specify field';
|
||||
}
|
||||
}
|
||||
this.selectors = e2e.pages.Panels.Visualization.Graph.VisualizationTab.selectors;
|
||||
}
|
||||
|
||||
setUnitFormat(axis: { format: any }) {
|
||||
|
59
public/e2e-tests/integration/smoketests.spec.ts
Normal file
59
public/e2e-tests/integration/smoketests.spec.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { e2e } from '@grafana/e2e';
|
||||
import { ScenarioContext } from '@grafana/e2e/src/support';
|
||||
|
||||
e2e.scenario({
|
||||
describeName: 'Smoke tests',
|
||||
itName: 'Login scenario, create test data source, dashboard, panel, and export scenario',
|
||||
addScenarioDataSource: true,
|
||||
addScenarioDashBoard: true,
|
||||
skipScenario: false,
|
||||
scenario: ({ dataSourceName, dashboardTitle, dashboardUid }: ScenarioContext) => {
|
||||
e2e.flows.openDashboard(dashboardTitle);
|
||||
e2e.pages.Dashboard.toolbarItems('Add panel').click();
|
||||
e2e.pages.AddDashboard.ctaButtons('Add Query').click();
|
||||
|
||||
e2e.pages.Panels.EditPanel.tabItems('Queries').click();
|
||||
e2e.pages.Panels.DataSource.TestData.QueryTab.scenarioSelect().select('CSV Metric Values');
|
||||
|
||||
e2e.pages.Panels.EditPanel.tabItems('Visualization').click();
|
||||
|
||||
e2e.pages.Panels.Visualization.Graph.VisualizationTab.xAxisSection()
|
||||
.contains('Show')
|
||||
.click();
|
||||
|
||||
e2e.flows.saveDashboard();
|
||||
|
||||
e2e.pages.Dashboard.backArrow().click();
|
||||
|
||||
e2e.pages.Panels.Panel.title('Panel Title').click();
|
||||
|
||||
e2e.pages.Panels.Panel.headerItems('Share').click();
|
||||
|
||||
e2e.pages.SharePanelModal.linkToRenderedImage().then(($a: any) => {
|
||||
// extract the fully qualified href property
|
||||
const url = $a.prop('href');
|
||||
|
||||
// Test that the image renderer returns 200 OK
|
||||
e2e().request({ method: 'GET', url, timeout: 120000 });
|
||||
|
||||
// Download image
|
||||
if (!e2e.env('CIRCLE_SHA1')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const theOutputImage = `${e2e.config().screenshotsFolder}/theOutput/smoke-test-scenario.png`;
|
||||
const theTruthImage = `${e2e.config().screenshotsFolder}/theTruth/smoke-test-scenario.png`;
|
||||
|
||||
e2e().wrap(
|
||||
e2e.imgSrcToBlob(url).then((blob: any) => {
|
||||
e2e.blobToBase64String(blob).then((base64String: string) => {
|
||||
const data = base64String.replace(/^data:image\/\w+;base64,/, '');
|
||||
e2e().writeFile(theOutputImage, data, 'base64');
|
||||
});
|
||||
})
|
||||
);
|
||||
e2e().wait(1000); // give the io a chance to flush image to disk
|
||||
e2e().compareSnapshot({ pathToFileA: theOutputImage, pathToFileB: theTruthImage });
|
||||
});
|
||||
},
|
||||
});
|
BIN
public/e2e-tests/screenShots/theTruth/smoke-test-scenario.png
Normal file
BIN
public/e2e-tests/screenShots/theTruth/smoke-test-scenario.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
4
public/e2e-tests/tsconfig.json
Normal file
4
public/e2e-tests/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["**/*.ts"]
|
||||
}
|
Loading…
Reference in New Issue
Block a user