Commit 04249ae7 by Steven Vachon Committed by GitHub

@grafana/e2e: improvements (#26939)

* Minor changes

* Added an `editPanel` flow function

... and moved the internals of `addPanel` to a common function for use by both

* Added optional template variables to `addDashboard` config

* Use latest Cypress 4.x version

* Updated lockfile
parent a3c34842
......@@ -49,7 +49,7 @@
"@mochajs/json-file-reporter": "^1.2.0",
"blink-diff": "1.0.13",
"commander": "5.0.0",
"cypress": "^4.9.0",
"cypress": "^4.12.1",
"cypress-file-upload": "^4.0.7",
"execa": "4.0.0",
"resolve-as-bin": "2.1.0",
......
......@@ -8,8 +8,21 @@ export interface AddDashboardConfig {
timeRange: DashboardTimeRangeConfig;
timezone: string;
title: string;
variables: Partial<AddVariableConfig>[];
}
export interface AddVariableConfig {
constantValue?: string;
dataSource?: string;
hide?: string;
label?: string;
name: string;
query?: string;
regex?: string;
type: string;
}
// @todo improve config input/output: https://stackoverflow.com/a/63507459/923745
// @todo this actually returns type `Cypress.Chainable`
export const addDashboard = (config?: Partial<AddDashboardConfig>): any => {
const fullConfig = {
......@@ -19,10 +32,11 @@ export const addDashboard = (config?: Partial<AddDashboardConfig>): any => {
},
timezone: 'Coordinated Universal Time',
title: `e2e-${Date.now()}`,
variables: [],
...config,
} as AddDashboardConfig;
const { timeRange, timezone, title } = fullConfig;
const { timeRange, timezone, title, variables } = fullConfig;
e2e().logToConsole('Adding dashboard with title:', title);
......@@ -33,11 +47,11 @@ export const addDashboard = (config?: Partial<AddDashboardConfig>): any => {
// @todo use the time range picker's time zone control
selectOption(e2e.pages.Dashboard.Settings.General.timezone(), timezone);
addVariables(variables);
e2e.components.BackButton.backArrow().click();
if (timeRange) {
setDashboardTimeRange(timeRange);
}
setDashboardTimeRange(timeRange);
e2e.pages.Dashboard.Toolbar.toolbarItems('Save dashboard').click();
......@@ -62,9 +76,79 @@ export const addDashboard = (config?: Partial<AddDashboardConfig>): any => {
});
// @todo remove `wrap` when possible
return e2e().wrap({
config: fullConfig,
uid,
});
return e2e().wrap(
{
config: fullConfig,
uid,
},
{ log: false }
);
});
};
export const VARIABLE_TYPE_AD_HOC_FILTERS = 'Ad hoc filters';
export const VARIABLE_TYPE_CONSTANT = 'Constant';
export const VARIABLE_TYPE_DATASOURCE = 'Datasource';
export const VARIABLE_TYPE_QUERY = 'Query';
const addVariable = (config: Partial<AddVariableConfig>, isFirst: boolean): any => {
const fullConfig = {
type: VARIABLE_TYPE_QUERY,
...config,
} as AddVariableConfig;
if (isFirst) {
e2e.pages.Dashboard.Settings.Variables.List.addVariableCTA().click();
} else {
e2e.pages.Dashboard.Settings.Variables.List.newButton().click();
}
const { constantValue, dataSource, hide, label, name, query, regex, type } = fullConfig;
if (hide) {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalHideSelect().select(hide);
}
if (label) {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput().type(label);
}
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().type(name);
if (type !== VARIABLE_TYPE_QUERY) {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect().select(type);
}
if (
dataSource &&
(type === VARIABLE_TYPE_AD_HOC_FILTERS || type === VARIABLE_TYPE_DATASOURCE || type === VARIABLE_TYPE_QUERY)
) {
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect().select(dataSource);
}
if (constantValue && type === VARIABLE_TYPE_CONSTANT) {
e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInput().type(constantValue);
}
if (type === VARIABLE_TYPE_QUERY) {
if (query) {
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput().type(query);
}
if (regex) {
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInput().type(regex);
}
}
e2e.pages.Dashboard.Settings.Variables.Edit.General.addButton().click();
return fullConfig;
};
const addVariables = (configs: Partial<AddVariableConfig>[]): any => {
if (configs.length > 0) {
e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click();
}
return configs.map((config, i) => addVariable(config, i === 0));
};
......@@ -14,6 +14,7 @@ export interface AddDataSourceConfig {
type: string;
}
// @todo improve config input/output: https://stackoverflow.com/a/63507459/923745
// @todo this actually returns type `Cypress.Chainable`
export const addDataSource = (config?: Partial<AddDataSourceConfig>): any => {
const fullConfig = {
......@@ -110,9 +111,12 @@ export const addDataSource = (config?: Partial<AddDataSourceConfig>): any => {
}
// @todo remove `wrap` when possible
return e2e().wrap({
config: fullConfig,
id,
});
return e2e().wrap(
{
config: fullConfig,
id,
},
{ log: false }
);
});
};
import { e2e } from '../index';
import { getLocalStorage, requireLocalStorage } from '../support/localStorage';
import { configurePanel, ConfigurePanelConfig } from './configurePanel';
import { getScenarioContext } from '../support/scenarioContext';
import { selectOption } from './selectOption';
export interface AddPanelConfig {
chartData: {
method: string;
route: string | RegExp;
};
dashboardUid: string;
export interface AddPanelConfig extends ConfigurePanelConfig {
dataSourceName: string;
matchScreenshot: boolean;
queriesForm: (config: AddPanelConfig) => void;
panelTitle: string;
screenshotName: string;
visualizationName: string;
}
// @todo improve config input/output: https://stackoverflow.com/a/63507459/923745
// @todo this actually returns type `Cypress.Chainable`
export const addPanel = (config?: Partial<AddPanelConfig>): any =>
getScenarioContext().then(({ lastAddedDashboardUid, lastAddedDataSource }: any) => {
const fullConfig = {
chartData: {
method: 'POST',
route: '/api/ds/query',
},
dashboardUid: lastAddedDashboardUid,
dataSourceName: lastAddedDataSource,
matchScreenshot: false,
panelTitle: `e2e-${Date.now()}`,
queriesForm: () => {},
screenshotName: 'chart',
visualizationName: 'Table',
...config,
} as AddPanelConfig;
const {
chartData,
dashboardUid,
dataSourceName,
matchScreenshot,
panelTitle,
queriesForm,
screenshotName,
visualizationName,
} = fullConfig;
e2e.flows.openDashboard({ uid: dashboardUid });
e2e.pages.Dashboard.Toolbar.toolbarItems('Add panel').click();
e2e.pages.AddDashboard.addNewPanel().click();
e2e().server();
// @todo alias '/**/*.js*' as '@pluginModule' when possible: https://github.com/cypress-io/cypress/issues/1296
e2e()
.route(chartData.method, chartData.route)
.as('chartData');
selectOption(e2e.components.DataSourcePicker.container(), dataSourceName);
// @todo instead wait for '@pluginModule'
e2e().wait(2000);
openOptions();
openOptionsGroup('settings');
getOptionsGroup('settings')
.find('[value="Panel Title"]')
.scrollIntoView()
.clear()
.type(panelTitle);
closeOptionsGroup('settings');
openOptionsGroup('type');
e2e.components.PluginVisualization.item(visualizationName)
.scrollIntoView()
.click();
closeOptionsGroup('type');
closeOptions();
queriesForm(fullConfig);
e2e().wait('@chartData');
// @todo enable when plugins have this implemented
//e2e.components.QueryEditorRow.actionButton('Disable/enable query').click();
//e2e.components.Panels.Panel.containerByTitle(panelTitle).find('.panel-content').contains('No data');
//e2e.components.QueryEditorRow.actionButton('Disable/enable query').click();
e2e()
.get('button[title="Apply changes and go back to dashboard"]')
.click();
e2e().wait('@chartData');
// Wait for RxJS
e2e().wait(500);
if (matchScreenshot) {
e2e.components.Panels.Panel.containerByTitle(panelTitle)
.find('.panel-content')
.screenshot(screenshotName);
e2e().compareScreenshots(screenshotName);
}
// @todo remove `wrap` when possible
return e2e().wrap({ config: fullConfig });
});
// @todo this actually returns type `Cypress.Chainable`
const closeOptions = (): any =>
isOptionsOpen().then((isOpen: any) => {
if (isOpen) {
e2e.components.PanelEditor.OptionsPane.close().click();
}
});
// @todo this actually returns type `Cypress.Chainable`
const closeOptionsGroup = (name: string): any =>
isOptionsGroupOpen(name).then((isOpen: any) => {
if (isOpen) {
toggleOptionsGroup(name);
}
});
const getOptionsGroup = (name: string) => e2e().get(`.options-group:has([aria-label="Options group Panel ${name}"])`);
// @todo this actually returns type `Cypress.Chainable`
const isOptionsGroupOpen = (name: string): any =>
requireLocalStorage(`grafana.dashboard.editor.ui.optionGroup[Panel ${name}]`).then(({ defaultToClosed }: any) => {
// @todo remove `wrap` when possible
return e2e().wrap(!defaultToClosed);
});
// @todo this actually returns type `Cypress.Chainable`
const isOptionsOpen = (): any =>
getLocalStorage('grafana.dashboard.editor.ui').then((data: any) => {
if (data) {
// @todo remove `wrap` when possible
return e2e().wrap(data.isPanelOptionsVisible);
} else {
// @todo remove `wrap` when possible
return e2e().wrap(true);
}
});
// @todo this actually returns type `Cypress.Chainable`
const openOptions = (): any =>
isOptionsOpen().then((isOpen: any) => {
if (!isOpen) {
e2e.components.PanelEditor.OptionsPane.open().click();
}
});
// @todo this actually returns type `Cypress.Chainable`
const openOptionsGroup = (name: string): any =>
isOptionsGroupOpen(name).then((isOpen: any) => {
if (!isOpen) {
toggleOptionsGroup(name);
}
});
const toggleOptionsGroup = (name: string) =>
getOptionsGroup(name)
.find('.editor-options-group-toggle')
.click();
getScenarioContext().then(({ lastAddedDataSource }: any) =>
configurePanel(
{
dataSourceName: lastAddedDataSource,
panelTitle: `e2e-${Date.now()}`,
visualizationName: 'Table',
...config,
} as AddPanelConfig,
false
)
);
import { e2e } from '../index';
import { getLocalStorage, requireLocalStorage } from '../support/localStorage';
import { getScenarioContext } from '../support/scenarioContext';
import { selectOption } from './selectOption';
export interface ConfigurePanelConfig {
chartData: {
method: string;
route: string | RegExp;
};
dashboardUid: string;
dataSourceName?: string;
matchScreenshot: boolean;
queriesForm?: (config: any) => void;
panelTitle: string;
screenshotName: string;
visitDashboardAtStart: boolean; // @todo remove when possible
visualizationName?: string;
}
// @todo improve config input/output: https://stackoverflow.com/a/63507459/923745
// @todo this actually returns type `Cypress.Chainable`
export const configurePanel = (config: Partial<ConfigurePanelConfig>, edit: boolean): any =>
getScenarioContext().then(({ lastAddedDashboardUid }: any) => {
const fullConfig = {
chartData: {
method: 'POST',
route: '/api/ds/query',
},
dashboardUid: lastAddedDashboardUid,
matchScreenshot: false,
saveDashboard: true,
screenshotName: 'chart',
visitDashboardAtStart: true,
...config,
} as ConfigurePanelConfig;
const {
chartData,
dashboardUid,
dataSourceName,
matchScreenshot,
panelTitle,
queriesForm,
screenshotName,
visitDashboardAtStart,
visualizationName,
} = fullConfig;
if (visitDashboardAtStart) {
e2e.flows.openDashboard({ uid: dashboardUid });
}
if (edit) {
e2e.components.Panels.Panel.title(panelTitle).click();
e2e.components.Panels.Panel.headerItems('Edit').click();
} else {
e2e.pages.Dashboard.Toolbar.toolbarItems('Add panel').click();
e2e.pages.AddDashboard.addNewPanel().click();
}
e2e().server();
// @todo alias '/**/*.js*' as '@pluginModule' when possible: https://github.com/cypress-io/cypress/issues/1296
e2e()
.route(chartData.method, chartData.route)
.as('chartData');
if (dataSourceName) {
selectOption(e2e.components.DataSourcePicker.container(), dataSourceName);
}
// @todo instead wait for '@pluginModule'
e2e().wait(2000);
e2e().wait('@chartData');
// `panelTitle` is needed to edit the panel, and unlikely to have its value changed at that point
const changeTitle = panelTitle && !edit;
if (changeTitle || visualizationName) {
openOptions();
if (changeTitle) {
openOptionsGroup('settings');
getOptionsGroup('settings')
.find('[value="Panel Title"]')
.scrollIntoView()
.clear()
.type(panelTitle);
closeOptionsGroup('settings');
}
if (visualizationName) {
openOptionsGroup('type');
e2e.components.PluginVisualization.item(visualizationName)
.scrollIntoView()
.click();
closeOptionsGroup('type');
}
closeOptions();
} else {
// Options are consistently closed
closeOptions();
}
if (queriesForm) {
queriesForm(fullConfig);
e2e().wait('@chartData');
}
// @todo enable when plugins have this implemented
//e2e.components.QueryEditorRow.actionButton('Disable/enable query').click();
//e2e().wait('@chartData');
//e2e.components.Panels.Panel.containerByTitle(panelTitle).find('.panel-content').contains('No data');
//e2e.components.QueryEditorRow.actionButton('Disable/enable query').click();
//e2e().wait('@chartData');
e2e()
.get('button[title="Apply changes and go back to dashboard"]')
.click();
e2e()
.url()
.should('include', `/d/${dashboardUid}`);
e2e().wait('@chartData');
// Wait for RxJS
e2e().wait(500);
if (matchScreenshot) {
e2e.components.Panels.Panel.containerByTitle(panelTitle)
.find('.panel-content')
.screenshot(screenshotName);
e2e().compareScreenshots(screenshotName);
}
// @todo remove `wrap` when possible
return e2e().wrap({ config: fullConfig }, { log: false });
});
// @todo this actually returns type `Cypress.Chainable`
const closeOptions = (): any =>
isOptionsOpen().then((isOpen: any) => {
if (isOpen) {
e2e.components.PanelEditor.OptionsPane.close().click();
}
});
// @todo this actually returns type `Cypress.Chainable`
const closeOptionsGroup = (name: string): any =>
isOptionsGroupOpen(name).then((isOpen: any) => {
if (isOpen) {
toggleOptionsGroup(name);
}
});
const getOptionsGroup = (name: string) => e2e().get(`.options-group:has([aria-label="Options group Panel ${name}"])`);
// @todo this actually returns type `Cypress.Chainable`
const isOptionsGroupOpen = (name: string): any =>
requireLocalStorage(`grafana.dashboard.editor.ui.optionGroup[Panel ${name}]`).then(({ defaultToClosed }: any) => {
// @todo remove `wrap` when possible
return e2e().wrap(!defaultToClosed, { log: false });
});
// @todo this actually returns type `Cypress.Chainable`
const isOptionsOpen = (): any =>
getLocalStorage('grafana.dashboard.editor.ui').then((data: any) => {
if (data) {
// @todo remove `wrap` when possible
return e2e().wrap(data.isPanelOptionsVisible, { log: false });
} else {
// @todo remove `wrap` when possible
return e2e().wrap(true, { log: false });
}
});
// @todo this actually returns type `Cypress.Chainable`
const openOptions = (): any =>
isOptionsOpen().then((isOpen: any) => {
if (!isOpen) {
e2e.components.PanelEditor.OptionsPane.open().click();
}
});
// @todo this actually returns type `Cypress.Chainable`
const openOptionsGroup = (name: string): any =>
isOptionsGroupOpen(name).then((isOpen: any) => {
if (!isOpen) {
toggleOptionsGroup(name);
}
});
const toggleOptionsGroup = (name: string) =>
getOptionsGroup(name)
.find('.editor-options-group-toggle')
.click();
import { configurePanel, ConfigurePanelConfig } from './configurePanel';
export interface EditPanelConfig extends ConfigurePanelConfig {
queriesForm?: (config: EditPanelConfig) => void;
}
// @todo improve config input/output: https://stackoverflow.com/a/63507459/923745
// @todo this actually returns type `Cypress.Chainable`
export const editPanel = (config: Partial<EditPanelConfig>): any => configurePanel(config, true);
import { addDashboard } from './addDashboard';
import { addDataSource } from './addDataSource';
import { addPanel } from './addPanel';
import { assertSuccessNotification } from './assertSuccessNotification';
import { deleteDashboard } from './deleteDashboard';
import { deleteDataSource } from './deleteDataSource';
import { login } from './login';
import { openDashboard } from './openDashboard';
import { saveDashboard } from './saveDashboard';
import { openPanelMenuItem, PanelMenuItems } from './openPanelMenuItem';
import { revertAllChanges } from './revertAllChanges';
import { selectOption } from './selectOption';
export const Flows = {
addDashboard,
addDataSource,
addPanel,
assertSuccessNotification,
deleteDashboard,
deleteDataSource,
login,
openDashboard,
saveDashboard,
openPanelMenuItem,
PanelMenuItems,
revertAllChanges,
selectOption,
};
export * from './addDashboard';
export * from './addDataSource';
export * from './addPanel';
export * from './assertSuccessNotification';
export * from './deleteDashboard';
export * from './deleteDataSource';
export * from './editPanel';
export * from './login';
export * from './openDashboard';
export * from './openPanelMenuItem';
export * from './revertAllChanges';
export * from './saveDashboard';
export * from './selectOption';
......@@ -7,6 +7,7 @@ export interface OpenDashboardConfig {
uid: string;
}
// @todo improve config input/output: https://stackoverflow.com/a/63507459/923745
export const openDashboard = (config?: Partial<OpenDashboardConfig>) =>
getScenarioContext().then(({ lastAddedDashboardUid }: any) => {
const fullConfig = {
......@@ -23,5 +24,5 @@ export const openDashboard = (config?: Partial<OpenDashboardConfig>) =>
}
// @todo remove `wrap` when possible
return e2e().wrap({ config: fullConfig });
return e2e().wrap({ config: fullConfig }, { log: false });
});
......@@ -4,10 +4,10 @@
* @packageDocumentation
*/
import { e2eScenario, ScenarioArguments } from './support/scenario';
import { Flows } from './flows';
import { getScenarioContext, setScenarioContext } from './support/scenarioContext';
import { e2eFactory } from './support';
import { selectors } from '@grafana/e2e-selectors';
import * as flows from './flows';
const e2eObject = {
env: (args: string) => Cypress.env(args),
......@@ -17,7 +17,7 @@ const e2eObject = {
scenario: (args: ScenarioArguments) => e2eScenario(args),
pages: e2eFactory({ selectors: selectors.pages }),
components: e2eFactory({ selectors: selectors.components }),
flows: Flows,
flows,
getScenarioContext,
setScenarioContext,
};
......
......@@ -3,7 +3,7 @@ import { e2e } from '../index';
// @todo this actually returns type `Cypress.Chainable`
const get = (key: string): any =>
e2e()
.wrap({ getLocalStorage: () => localStorage.getItem(key) })
.wrap({ getLocalStorage: () => localStorage.getItem(key) }, { log: false })
.invoke('getLocalStorage');
// @todo this actually returns type `Cypress.Chainable`
......
import { e2e } from '../';
import { Flows } from '../flows';
export interface ScenarioArguments {
describeName: string;
......@@ -22,20 +21,20 @@ export const e2eScenario = ({
if (skipScenario) {
it.skip(itName, () => scenario());
} else {
before(() => Flows.login(e2e.env('USERNAME'), e2e.env('PASSWORD')));
before(() => e2e.flows.login(e2e.env('USERNAME'), e2e.env('PASSWORD')));
beforeEach(() => {
Cypress.Cookies.preserveOnce('grafana_session');
if (addScenarioDataSource) {
Flows.addDataSource();
e2e.flows.addDataSource();
}
if (addScenarioDashBoard) {
Flows.addDashboard();
e2e.flows.addDashboard();
}
});
afterEach(() => Flows.revertAllChanges());
afterEach(() => e2e.flows.revertAllChanges());
after(() => e2e().clearCookies());
it(itName, () => scenario());
......
......@@ -37,19 +37,25 @@ const lastProperty = <T extends DeleteDashboardConfig | DeleteDataSourceConfig,
// @todo this actually returns type `Cypress.Chainable`
export const getScenarioContext = (): any =>
e2e()
.wrap({
getScenarioContext: () => ({ ...scenarioContext } as ScenarioContext),
})
.invoke('getScenarioContext');
.wrap(
{
getScenarioContext: () => ({ ...scenarioContext } as ScenarioContext),
},
{ log: false }
)
.invoke({ log: false }, 'getScenarioContext');
// @todo this actually returns type `Cypress.Chainable`
export const setScenarioContext = (newContext: Partial<ScenarioContext>): any =>
e2e()
.wrap({
setScenarioContext: () => {
Object.entries(newContext).forEach(([key, value]) => {
scenarioContext[key] = value;
});
.wrap(
{
setScenarioContext: () => {
Object.entries(newContext).forEach(([key, value]) => {
scenarioContext[key] = value;
});
},
},
})
.invoke('setScenarioContext');
{ log: false }
)
.invoke({ log: false }, 'setScenarioContext');
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment