From aeeaa67b0a845bad327e6a31bcbd46fb57a81955 Mon Sep 17 00:00:00 2001 From: Sarah Zinger Date: Wed, 29 Sep 2021 10:27:49 -0400 Subject: [PATCH] E2E: improvements to the importDashboard flow (#39693) * E2E: improvements to the importDashboard flow --- e2e/suite1/dashboards/TestDashboard.json | 210 ++++++++++++++++++ e2e/suite1/specs/import-dashboard.spec.ts | 116 +--------- e2e/suite1/tsconfig.json | 3 +- packages/grafana-e2e/cypress/plugins/index.js | 15 ++ .../grafana-e2e/cypress/support/commands.ts | 7 + .../grafana-e2e/cypress/support/index.d.ts | 1 + .../grafana-e2e/src/flows/importDashboard.ts | 41 ++-- .../grafana-e2e/src/flows/importDashboards.ts | 19 ++ packages/grafana-e2e/src/flows/index.ts | 1 + 9 files changed, 281 insertions(+), 132 deletions(-) create mode 100644 e2e/suite1/dashboards/TestDashboard.json create mode 100644 packages/grafana-e2e/src/flows/importDashboards.ts diff --git a/e2e/suite1/dashboards/TestDashboard.json b/e2e/suite1/dashboards/TestDashboard.json new file mode 100644 index 00000000000..4706db47b67 --- /dev/null +++ b/e2e/suite1/dashboards/TestDashboard.json @@ -0,0 +1,210 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 321, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 6, + "options": { + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "text": {} + }, + "pluginVersion": "8.3.0-pre", + "title": "Gauge Example", + "type": "gauge" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "8.3.0-pre", + "title": "Stat", + "type": "stat" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "title": "Time series example", + "type": "timeseries" + } + ], + "refresh": false, + "schemaVersion": 31, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "2021-09-01T04:00:00.000Z", + "to": "2021-09-15T04:00:00.000Z" + }, + "timepicker": {}, + "timezone": "", + "title": "E2E Test - Import Dashboard", + "uid": "kquZN5H7k", + "version": 4 +} diff --git a/e2e/suite1/specs/import-dashboard.spec.ts b/e2e/suite1/specs/import-dashboard.spec.ts index 1bc6095d60b..477a64587c3 100644 --- a/e2e/suite1/specs/import-dashboard.spec.ts +++ b/e2e/suite1/specs/import-dashboard.spec.ts @@ -1,122 +1,12 @@ import { e2e } from '@grafana/e2e'; e2e.scenario({ - describeName: 'Import Dashboard Test', - itName: 'Ensure you can import a dashboard', + describeName: 'Import Dashboards Test', + itName: 'Ensure you can import a number of json test dashboards from a specific test directory', addScenarioDataSource: false, addScenarioDashBoard: false, skipScenario: false, scenario: () => { - e2e.flows.importDashboard(TEST_DASHBOARD); + e2e.flows.importDashboards('/dashboards', 1000); }, }); - -const TEST_DASHBOARD = { - annotations: { - list: [ - { - builtIn: 1, - datasource: '-- Grafana --', - enable: true, - hide: true, - iconColor: 'rgba(0, 211, 255, 1)', - name: 'Annotations & Alerts', - type: 'dashboard', - }, - ], - }, - editable: true, - gnetId: null, - graphTooltip: 0, - id: 74, - links: [], - panels: [ - { - datasource: null, - fieldConfig: { - defaults: { - color: { - mode: 'palette-classic', - }, - custom: { - axisLabel: '', - axisPlacement: 'auto', - barAlignment: 0, - drawStyle: 'line', - fillOpacity: 0, - gradientMode: 'none', - hideFrom: { - legend: false, - tooltip: false, - viz: false, - }, - lineInterpolation: 'linear', - lineWidth: 1, - pointSize: 5, - scaleDistribution: { - type: 'linear', - }, - showPoints: 'auto', - spanNulls: false, - stacking: { - group: 'A', - mode: 'none', - }, - thresholdsStyle: { - mode: 'off', - }, - }, - mappings: [], - thresholds: { - mode: 'absolute', - steps: [ - { - color: 'green', - value: null, - }, - { - color: 'red', - value: 80, - }, - ], - }, - }, - overrides: [], - }, - gridPos: { - h: 9, - w: 12, - x: 0, - y: 0, - }, - id: 2, - options: { - legend: { - calcs: [], - displayMode: 'list', - placement: 'bottom', - }, - tooltip: { - mode: 'single', - }, - }, - title: 'Panel Title', - type: 'timeseries', - }, - ], - schemaVersion: 30, - style: 'dark', - tags: [], - templating: { - list: [], - }, - time: { - from: '2021-06-30T04:00:00.000Z', - to: '2021-07-02T03:59:59.000Z', - }, - timepicker: {}, - timezone: '', - title: 'An imported dashboard for e2e tests', - uid: '6V0Nzyz7k', - version: 1, -}; diff --git a/e2e/suite1/tsconfig.json b/e2e/suite1/tsconfig.json index f1cce18f82a..247f35b0b86 100644 --- a/e2e/suite1/tsconfig.json +++ b/e2e/suite1/tsconfig.json @@ -3,5 +3,6 @@ "types": ["cypress"] }, "extends": "../../tsconfig.json", - "include": ["**/*.ts", "../../packages/grafana-e2e/cypress/support/index.d.ts"] + "include": ["**/*.ts", "../../packages/grafana-e2e/cypress/support/index.d.ts"], + "resolveJsonModule": true } diff --git a/packages/grafana-e2e/cypress/plugins/index.js b/packages/grafana-e2e/cypress/plugins/index.js index c671748a639..8b0f5dfdb46 100644 --- a/packages/grafana-e2e/cypress/plugins/index.js +++ b/packages/grafana-e2e/cypress/plugins/index.js @@ -1,3 +1,6 @@ +const fs = require('fs'); +const path = require('path'); + const compareScreenshots = require('./compareScreenshots'); const extendConfig = require('./extendConfig'); const readProvisions = require('./readProvisions'); @@ -12,6 +15,18 @@ module.exports = (on, config) => { return null; }, }); + on('task', { + getJSONFilesFromDir: async ({ projectPath, relativePath }) => { + const directoryPath = path.join(projectPath, relativePath); + const jsonFiles = fs.readdirSync(directoryPath); + return jsonFiles + .filter((fileName) => /.json$/i.test(fileName)) + .map((fileName) => { + const fileBuffer = fs.readFileSync(path.join(directoryPath, fileName)); + return JSON.parse(fileBuffer); + }); + }, + }); // Always extend with this library's config and return for diffing // @todo remove this when possible: https://github.com/cypress-io/cypress/issues/5674 diff --git a/packages/grafana-e2e/cypress/support/commands.ts b/packages/grafana-e2e/cypress/support/commands.ts index a390bbe43f4..8b44ce899ec 100644 --- a/packages/grafana-e2e/cypress/support/commands.ts +++ b/packages/grafana-e2e/cypress/support/commands.ts @@ -23,3 +23,10 @@ Cypress.Commands.add('readProvisions', (filePaths: string[]) => { filePaths, }); }); + +Cypress.Commands.add('getJSONFilesFromDir', (dirPath: string) => { + return cy.task('getJSONFilesFromDir', { + projectPath: Cypress.config().parentTestsFolder, + relativePath: dirPath, + }); +}); diff --git a/packages/grafana-e2e/cypress/support/index.d.ts b/packages/grafana-e2e/cypress/support/index.d.ts index 3a559aa5430..224b21bd339 100644 --- a/packages/grafana-e2e/cypress/support/index.d.ts +++ b/packages/grafana-e2e/cypress/support/index.d.ts @@ -5,5 +5,6 @@ declare namespace Cypress { compareScreenshots(config: CompareScreenshotsConfig | string): Chainable; logToConsole(message: string, optional?: any): void; readProvisions(filePaths: string[]): Chainable; + getJSONFilesFromDir(dirPath: string): Chainable; } } diff --git a/packages/grafana-e2e/src/flows/importDashboard.ts b/packages/grafana-e2e/src/flows/importDashboard.ts index abf47860900..d4eadd1c412 100644 --- a/packages/grafana-e2e/src/flows/importDashboard.ts +++ b/packages/grafana-e2e/src/flows/importDashboard.ts @@ -7,13 +7,14 @@ type Panel = { [key: string]: unknown; }; -type Dashboard = { title: string; panels: Panel[]; uid: string; [key: string]: unknown }; +export type Dashboard = { title: string; panels: Panel[]; uid: string; [key: string]: unknown }; /** - * Smoke test a datasource by quickly importing a test dashboard for it + * Smoke test a particular dashboard by quickly importing a json file and validate that all the panels finish loading * @param dashboardToImport a sample dashboard + * @param queryTimeout a number of ms to wait for the imported dashboard to finish loading */ -export const importDashboard = (dashboardToImport: Dashboard) => { +export const importDashboard = (dashboardToImport: Dashboard, queryTimeout?: number) => { e2e().visit(fromBaseUrl('/dashboard/import')); // Note: normally we'd use 'click' and then 'type' here, but the json object is so big that using 'val' is much faster @@ -24,7 +25,9 @@ export const importDashboard = (dashboardToImport: Dashboard) => { e2e.components.DashboardImportPage.submit().should('be.visible').click(); e2e.components.ImportDashboardForm.name().should('be.visible').click().clear().type(dashboardToImport.title); e2e.components.ImportDashboardForm.submit().should('be.visible').click(); - e2e().wait(3000); + + // wait for dashboard to load + e2e().wait(queryTimeout || 6000); // save the newly imported dashboard to context so it'll get properly deleted later e2e() @@ -42,19 +45,21 @@ export const importDashboard = (dashboardToImport: Dashboard) => { expect(dashboardToImport.uid).to.equal(uid); }); - // inspect first panel and verify data has been processed for it - e2e.components.Panels.Panel.title(dashboardToImport.panels[0].title).should('be.visible').click(); - e2e.components.Panels.Panel.headerItems('Inspect').should('be.visible').click(); - e2e.components.Tab.title('JSON').should('be.visible').click(); - e2e().wait(3000); - e2e.components.PanelInspector.Json.content().should('be.visible').contains('Panel JSON').click(); - e2e().wait(3000); - e2e.components.Select.option().should('be.visible').contains('Data').click(); - e2e().wait(3000); + dashboardToImport.panels.forEach((panel) => { + // Look at the json data + e2e.components.Panels.Panel.title(panel.title).should('be.visible').click(); + e2e.components.Panels.Panel.headerItems('Inspect').should('be.visible').click(); + e2e.components.Tab.title('JSON').should('be.visible').click(); + e2e.components.PanelInspector.Json.content().should('be.visible').contains('Panel JSON').click(); + e2e.components.Select.option().should('be.visible').contains('Data').click(); - // ensures that panel has loaded without knowingly hitting an error - // note: this does not prove that data came back as we expected it, - // it could get `state: Done` for no data for example - // but it ensures we didn't hit a 401 or 500 or something like that - e2e.components.CodeEditor.container().should('be.visible').contains('"state": "Done"'); + // ensures that panel has loaded without knowingly hitting an error + // note: this does not prove that data came back as we expected it, + // it could get `state: Done` for no data for example + // but it ensures we didn't hit a 401 or 500 or something like that + e2e.components.CodeEditor.container().should('be.visible').contains('"state": "Done"'); + + // need to close panel + e2e.components.Drawer.General.close().click(); + }); }; diff --git a/packages/grafana-e2e/src/flows/importDashboards.ts b/packages/grafana-e2e/src/flows/importDashboards.ts new file mode 100644 index 00000000000..6a4c2a5de85 --- /dev/null +++ b/packages/grafana-e2e/src/flows/importDashboards.ts @@ -0,0 +1,19 @@ +import { importDashboard, Dashboard } from './importDashboard'; +import { e2e } from '../index'; + +/** + * Smoke test several dashboard json files from a test directory + * and validate that all the panels in each import finish loading their queries + * @param dirPath the relative path to a directory which contains json files representing dashboards, + * for example if your dashboards live in `cypress/testDashboards` you can pass `/testDashboards` + * @param queryTimeout a number of ms to wait for the imported dashboard to finish loading + */ +export const importDashboards = async (dirPath: string, queryTimeout?: number) => { + e2e() + .getJSONFilesFromDir(dirPath) + .then((jsonFiles: Dashboard[]) => { + jsonFiles.forEach((file) => { + importDashboard(file, queryTimeout || 6000); + }); + }); +}; diff --git a/packages/grafana-e2e/src/flows/index.ts b/packages/grafana-e2e/src/flows/index.ts index 99885d3f2fd..83ba4e5a8b4 100644 --- a/packages/grafana-e2e/src/flows/index.ts +++ b/packages/grafana-e2e/src/flows/index.ts @@ -13,6 +13,7 @@ export * from './revertAllChanges'; export * from './saveDashboard'; export * from './selectOption'; export * from './importDashboard'; +export * from './importDashboards'; export { VISUALIZATION_ALERT_LIST,