Chore: Move to Cypress 12 and decouple cypress from @grafana/e2e (#74084)
* update drone to use cypress 12 image * upgrade cypress to 12 in core * cypress config actually valid * update @grafana/e2e imports and add lint rule * ignore grafana-e2e from betterer now it's deprecated * fix remaining type errors * fix failing tests * remove unnecessary tsconfig * remove unnecessary comment * update enterprise suite commands to work * add cypress config to CODEOWNERS * export setTimeRange in utils * remove @grafana/e2e from core deps * try running the command through yarn * move CMD to scripts * Update cloud-data-sources e2e image * Update paths --------- Co-authored-by: Andreas Christou <andreas.christou@grafana.com>
This commit is contained in:
co-authored by
Andreas Christou
parent
e7a2c95586
commit
0f2f25c5d9
@@ -0,0 +1,79 @@
|
||||
import { e2e } from '../';
|
||||
|
||||
export interface BenchmarkArguments {
|
||||
name: string;
|
||||
dashboard: {
|
||||
folder: string;
|
||||
delayAfterOpening: number;
|
||||
skipPanelValidation: boolean;
|
||||
};
|
||||
repeat: number;
|
||||
duration: number;
|
||||
appStats?: {
|
||||
startCollecting?: (window: Window) => void;
|
||||
collect: (window: Window) => Record<string, unknown>;
|
||||
};
|
||||
skipScenario?: boolean;
|
||||
}
|
||||
|
||||
export const benchmark = ({
|
||||
name,
|
||||
skipScenario = false,
|
||||
repeat,
|
||||
duration,
|
||||
appStats,
|
||||
dashboard,
|
||||
}: BenchmarkArguments) => {
|
||||
if (skipScenario) {
|
||||
describe(name, () => {
|
||||
it.skip(name, () => {});
|
||||
});
|
||||
} else {
|
||||
describe(name, () => {
|
||||
before(() => {
|
||||
cy.session('login', () => e2e.flows.login(e2e.env('USERNAME'), e2e.env('PASSWORD'), true), {
|
||||
cacheAcrossSpecs: true,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
e2e.flows.importDashboards(dashboard.folder, 1000, dashboard.skipPanelValidation);
|
||||
});
|
||||
|
||||
afterEach(() => e2e.flows.revertAllChanges());
|
||||
|
||||
Array(repeat)
|
||||
.fill(0)
|
||||
.map((_, i) => {
|
||||
const testName = `${name}-${i}`;
|
||||
return it(testName, () => {
|
||||
e2e.flows.openDashboard();
|
||||
|
||||
e2e().wait(dashboard.delayAfterOpening);
|
||||
|
||||
if (appStats) {
|
||||
const startCollecting = appStats.startCollecting;
|
||||
if (startCollecting) {
|
||||
e2e()
|
||||
.window()
|
||||
.then((win) => startCollecting(win));
|
||||
}
|
||||
|
||||
e2e().startBenchmarking(testName);
|
||||
e2e().wait(duration);
|
||||
|
||||
e2e()
|
||||
.window()
|
||||
.then((win) => {
|
||||
e2e().stopBenchmarking(testName, appStats.collect(win));
|
||||
});
|
||||
} else {
|
||||
e2e().startBenchmarking(testName);
|
||||
e2e().wait(duration);
|
||||
e2e().stopBenchmarking(testName, {});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './localStorage';
|
||||
export * from './scenarioContext';
|
||||
export * from './selector';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,23 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
// @todo this actually returns type `Cypress.Chainable`
|
||||
const get = (key: string): any =>
|
||||
e2e()
|
||||
.wrap({ getLocalStorage: () => localStorage.getItem(key) }, { log: false })
|
||||
.invoke('getLocalStorage');
|
||||
|
||||
// @todo this actually returns type `Cypress.Chainable`
|
||||
export const getLocalStorage = (key: string): any =>
|
||||
get(key).then((value: any) => {
|
||||
if (value === null) {
|
||||
return value;
|
||||
} else {
|
||||
return JSON.parse(value);
|
||||
}
|
||||
});
|
||||
|
||||
// @todo this actually returns type `Cypress.Chainable`
|
||||
export const requireLocalStorage = (key: string): any =>
|
||||
get(key) // `getLocalStorage()` would turn 'null' into `null`
|
||||
.should('not.equal', null)
|
||||
.then((value: any) => JSON.parse(value as string));
|
||||
@@ -0,0 +1,53 @@
|
||||
import { e2e } from '../';
|
||||
|
||||
export interface ScenarioArguments {
|
||||
describeName: string;
|
||||
itName: string;
|
||||
scenario: Function;
|
||||
skipScenario?: boolean;
|
||||
addScenarioDataSource?: boolean;
|
||||
addScenarioDashBoard?: boolean;
|
||||
loginViaApi?: boolean;
|
||||
}
|
||||
|
||||
export const e2eScenario = ({
|
||||
describeName,
|
||||
itName,
|
||||
scenario,
|
||||
skipScenario = false,
|
||||
addScenarioDataSource = false,
|
||||
addScenarioDashBoard = false,
|
||||
loginViaApi = true,
|
||||
}: ScenarioArguments) => {
|
||||
describe(describeName, () => {
|
||||
if (skipScenario) {
|
||||
it.skip(itName, () => scenario());
|
||||
} else {
|
||||
before(() => {
|
||||
cy.session(
|
||||
'login',
|
||||
() => {
|
||||
e2e.flows.login(e2e.env('USERNAME'), e2e.env('PASSWORD'), loginViaApi);
|
||||
},
|
||||
{
|
||||
cacheAcrossSpecs: true,
|
||||
}
|
||||
);
|
||||
e2e.flows.setDefaultUserPreferences();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (addScenarioDataSource) {
|
||||
e2e.flows.addDataSource();
|
||||
}
|
||||
if (addScenarioDashBoard) {
|
||||
e2e.flows.addDashboard();
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => e2e.flows.revertAllChanges());
|
||||
|
||||
it(itName, () => scenario());
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { DeleteDashboardConfig } from '../flows/deleteDashboard';
|
||||
import { DeleteDataSourceConfig } from '../flows/deleteDataSource';
|
||||
import { e2e } from '../index';
|
||||
|
||||
export interface ScenarioContext {
|
||||
addedDashboards: DeleteDashboardConfig[];
|
||||
addedDataSources: DeleteDataSourceConfig[];
|
||||
lastAddedDashboard: string; // @todo rename to `lastAddedDashboardTitle`
|
||||
lastAddedDashboardUid: string;
|
||||
lastAddedDataSource: string; // @todo rename to `lastAddedDataSourceName`
|
||||
lastAddedDataSourceId: string;
|
||||
hasChangedUserPreferences: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const scenarioContext: ScenarioContext = {
|
||||
addedDashboards: [],
|
||||
addedDataSources: [],
|
||||
hasChangedUserPreferences: false,
|
||||
get lastAddedDashboard() {
|
||||
return lastProperty(this.addedDashboards, 'title');
|
||||
},
|
||||
get lastAddedDashboardUid() {
|
||||
return lastProperty(this.addedDashboards, 'uid');
|
||||
},
|
||||
get lastAddedDataSource() {
|
||||
return lastProperty(this.addedDataSources, 'name');
|
||||
},
|
||||
get lastAddedDataSourceId() {
|
||||
return lastProperty(this.addedDataSources, 'id');
|
||||
},
|
||||
};
|
||||
|
||||
const lastProperty = <T extends DeleteDashboardConfig | DeleteDataSourceConfig, K extends keyof T>(
|
||||
items: T[],
|
||||
key: K
|
||||
) => items[items.length - 1]?.[key] ?? '';
|
||||
|
||||
export const getScenarioContext = (): Cypress.Chainable<ScenarioContext> =>
|
||||
e2e()
|
||||
.wrap(
|
||||
{
|
||||
getScenarioContext: (): ScenarioContext => ({ ...scenarioContext }),
|
||||
},
|
||||
{ log: false }
|
||||
)
|
||||
.invoke({ log: false }, 'getScenarioContext');
|
||||
|
||||
export const setScenarioContext = (newContext: Partial<ScenarioContext>): Cypress.Chainable<ScenarioContext> =>
|
||||
e2e()
|
||||
.wrap(
|
||||
{
|
||||
setScenarioContext: () => {
|
||||
Object.entries(newContext).forEach(([key, value]) => {
|
||||
scenarioContext[key] = value;
|
||||
});
|
||||
},
|
||||
},
|
||||
{ log: false }
|
||||
)
|
||||
.invoke({ log: false }, 'setScenarioContext');
|
||||
@@ -0,0 +1,11 @@
|
||||
export interface SelectorApi {
|
||||
fromAriaLabel: (selector: string) => string;
|
||||
fromDataTestId: (selector: string) => string;
|
||||
fromSelector: (selector: string) => string;
|
||||
}
|
||||
|
||||
export const Selector: SelectorApi = {
|
||||
fromAriaLabel: (selector: string) => `[aria-label="${selector}"]`,
|
||||
fromDataTestId: (selector: string) => `[data-testid="${selector}"]`,
|
||||
fromSelector: (selector: string) => selector,
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import { CssSelector, FunctionSelector, Selectors, StringSelector, UrlSelector } from '@grafana/e2e-selectors';
|
||||
|
||||
import { e2e } from '../index';
|
||||
|
||||
import { Selector } from './selector';
|
||||
import { fromBaseUrl } from './url';
|
||||
|
||||
export type VisitFunction = (args?: string, queryParams?: object) => Cypress.Chainable<Window>;
|
||||
export type E2EVisit = { visit: VisitFunction };
|
||||
export type E2EFunction = ((text?: string, options?: CypressOptions) => Cypress.Chainable<JQuery<HTMLElement>>) &
|
||||
E2EFunctionWithOnlyOptions;
|
||||
export type E2EFunctionWithOnlyOptions = (options?: CypressOptions) => Cypress.Chainable<JQuery<HTMLElement>>;
|
||||
|
||||
export type TypeSelectors<S> = S extends StringSelector
|
||||
? E2EFunctionWithOnlyOptions
|
||||
: S extends FunctionSelector
|
||||
? E2EFunction
|
||||
: S extends CssSelector
|
||||
? E2EFunction
|
||||
: S extends UrlSelector
|
||||
? E2EVisit & Omit<E2EFunctions<S>, 'url'>
|
||||
: S extends Record<any, any>
|
||||
? E2EFunctions<S>
|
||||
: S;
|
||||
|
||||
export type E2EFunctions<S extends Selectors> = {
|
||||
[P in keyof S]: TypeSelectors<S[P]>;
|
||||
};
|
||||
|
||||
export type E2EObjects<S extends Selectors> = E2EFunctions<S>;
|
||||
|
||||
export type E2EFactoryArgs<S extends Selectors> = { selectors: S };
|
||||
|
||||
export type CypressOptions = Partial<Cypress.Loggable & Cypress.Timeoutable & Cypress.Withinable & Cypress.Shadow>;
|
||||
|
||||
const processSelectors = <S extends Selectors>(e2eObjects: E2EFunctions<S>, selectors: S): E2EFunctions<S> => {
|
||||
const logOutput = (data: any) => e2e().logToConsole('Retrieving Selector:', data);
|
||||
const keys = Object.keys(selectors);
|
||||
for (let index = 0; index < keys.length; index++) {
|
||||
const key = keys[index];
|
||||
const value = selectors[key];
|
||||
|
||||
if (key === 'url') {
|
||||
// @ts-ignore
|
||||
e2eObjects['visit'] = (args?: string, queryParams?: object) => {
|
||||
let parsedUrl = '';
|
||||
if (typeof value === 'string') {
|
||||
parsedUrl = fromBaseUrl(value);
|
||||
}
|
||||
|
||||
if (typeof value === 'function' && args) {
|
||||
parsedUrl = fromBaseUrl(value(args));
|
||||
}
|
||||
|
||||
e2e().logToConsole('Visiting', parsedUrl);
|
||||
if (queryParams) {
|
||||
return e2e().visit({ url: parsedUrl, qs: queryParams });
|
||||
} else {
|
||||
return e2e().visit(parsedUrl);
|
||||
}
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
// @ts-ignore
|
||||
e2eObjects[key] = (options?: CypressOptions) => {
|
||||
logOutput(value);
|
||||
const selector = value.startsWith('data-testid')
|
||||
? Selector.fromDataTestId(value)
|
||||
: Selector.fromAriaLabel(value);
|
||||
|
||||
return e2e().get(selector, options);
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === 'function') {
|
||||
// @ts-ignore
|
||||
e2eObjects[key] = function (textOrOptions?: string | CypressOptions, options?: CypressOptions) {
|
||||
// the input can only be ()
|
||||
if (arguments.length === 0) {
|
||||
const selector = value(undefined as unknown as string);
|
||||
|
||||
logOutput(selector);
|
||||
return e2e().get(selector);
|
||||
}
|
||||
|
||||
// the input can be (text) or (options)
|
||||
if (arguments.length === 1) {
|
||||
if (typeof textOrOptions === 'string') {
|
||||
const selectorText = value(textOrOptions);
|
||||
const selector = selectorText.startsWith('data-testid')
|
||||
? Selector.fromDataTestId(selectorText)
|
||||
: Selector.fromAriaLabel(selectorText);
|
||||
|
||||
logOutput(selector);
|
||||
return e2e().get(selector);
|
||||
}
|
||||
const selector = value(undefined as unknown as string);
|
||||
|
||||
logOutput(selector);
|
||||
return e2e().get(selector, textOrOptions);
|
||||
}
|
||||
|
||||
// the input can only be (text, options)
|
||||
if (arguments.length === 2 && typeof textOrOptions === 'string') {
|
||||
const text = textOrOptions;
|
||||
const selectorText = value(text);
|
||||
const selector = text.startsWith('data-testid')
|
||||
? Selector.fromDataTestId(selectorText)
|
||||
: Selector.fromAriaLabel(selectorText);
|
||||
|
||||
logOutput(selector);
|
||||
return e2e().get(selector, options);
|
||||
}
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
// @ts-ignore
|
||||
e2eObjects[key] = processSelectors({}, value);
|
||||
}
|
||||
}
|
||||
|
||||
return e2eObjects;
|
||||
};
|
||||
|
||||
export const e2eFactory = <S extends Selectors>({ selectors }: E2EFactoryArgs<S>): E2EObjects<S> => {
|
||||
const e2eObjects: E2EFunctions<S> = {} as E2EFunctions<S>;
|
||||
processSelectors(e2eObjects, selectors);
|
||||
|
||||
return { ...e2eObjects };
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { e2e } from '../index';
|
||||
|
||||
const getBaseUrl = () => e2e.env('BASE_URL') || e2e.config().baseUrl || 'http://localhost:3000';
|
||||
|
||||
export const fromBaseUrl = (url = '') => new URL(url, getBaseUrl()).href;
|
||||
|
||||
export const getDashboardUid = (url: string): string => {
|
||||
const matches = new URL(url).pathname.match(/\/d\/([^/]+)/);
|
||||
if (!matches) {
|
||||
throw new Error(`Couldn't parse uid from ${url}`);
|
||||
} else {
|
||||
return matches[1];
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user