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:
@@ -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),
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -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');
|
||||
};
|
||||
@@ -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.`);
|
||||
}
|
||||
});
|
||||
*/
|
||||
};
|
||||
@@ -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.`);
|
||||
}
|
||||
});
|
||||
*/
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
export const openDashboard = (dashboardTitle: string) => {
|
||||
e2e.pages.Dashboards.visit();
|
||||
e2e.pages.Dashboards.dashboards(dashboardTitle).click();
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import { e2e } from './noTypeCheck';
|
||||
|
||||
export { e2e };
|
||||
@@ -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);
|
||||
@@ -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}`,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const AddDataSource = pageFactory({
|
||||
url: '/datasources/new',
|
||||
selectors: {
|
||||
dataSourcePlugins: (pluginName: string) => `Data source plugin item ${pluginName}`,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const ConfirmModal = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
delete: 'Confirm Modal Danger Button',
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const Dashboards = pageFactory({
|
||||
url: '/dashboards',
|
||||
selectors: {
|
||||
dashboards: (title: string) => `Dashboard search item ${title}`,
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const EditPanel = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
tabItems: (text: string) => `Edit panel tab item ${text}`,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import { VisualizationTab } from './visualizationTab';
|
||||
|
||||
export const Graph = {
|
||||
VisualizationTab,
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { pageFactory } from '../../support';
|
||||
|
||||
export const VisualizationTab = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
xAxisSection: 'X-Axis section',
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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}`,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const SaveDashboardAsModal = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
newName: 'Save dashboard title field',
|
||||
save: 'Save dashboard button',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const SaveDashboardModal = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
save: 'Dashboard settings Save Dashboard Modal Save button',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const SharePanelModal = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
linkToRenderedImage: 'Link to rendered image',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import { QueryTab } from './queryTab';
|
||||
|
||||
export const TestData = {
|
||||
QueryTab,
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { pageFactory } from '../../support';
|
||||
|
||||
export const QueryTab = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
scenarioSelect: 'Test Data Query scenario select',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ScenarioContext } from './scenario';
|
||||
|
||||
export { ScenarioContext };
|
||||
export * from './types';
|
||||
export * from './selector';
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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];
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user