Dashboards: Enable scenes by default (#93818)

* Mark Scenes feature toggles as GA

* Move old arch e2e to a new folder

* Run E2E on scenes by default

* Upgrade e2e-selectors to ensure the tests in Playwright works
This commit is contained in:
Ivan Ortega Alba
2024-09-30 10:49:02 +01:00
committed by GitHub
parent 42f1fcaf2c
commit 7bca69849f
163 changed files with 748 additions and 747 deletions
+70
View File
@@ -0,0 +1,70 @@
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, () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
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();
cy.wait(dashboard.delayAfterOpening);
if (appStats) {
const startCollecting = appStats.startCollecting;
if (startCollecting) {
cy.window().then((win) => startCollecting(win));
}
cy.startBenchmarking(testName);
cy.wait(duration);
cy.window().then((win) => {
cy.stopBenchmarking(testName, appStats.collect(win));
});
} else {
cy.startBenchmarking(testName);
cy.wait(duration);
cy.stopBenchmarking(testName, {});
}
});
});
});
}
};
+29
View File
@@ -0,0 +1,29 @@
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
copyToClipboard(): Chainable;
copyFromClipboard(): Chainable;
}
}
}
Cypress.Commands.add('copyFromClipboard', () => {
return cy.window().then((win) => {
return cy.wrap(win.navigator.clipboard.readText());
});
});
Cypress.Commands.add(
'copyToClipboard',
{
prevSubject: [],
},
(subject: string) => {
return cy.window().then((win) => {
return cy.wrap(win.navigator.clipboard.writeText(subject));
});
}
);
export {};
+4
View File
@@ -0,0 +1,4 @@
export * from './localStorage';
export * from './scenarioContext';
export * from './selector';
export * from './types';
@@ -0,0 +1,17 @@
// @todo this actually returns type `Cypress.Chainable`
const get = (key: string) =>
cy.wrap({ getLocalStorage: () => localStorage.getItem(key) }, { log: false }).invoke('getLocalStorage');
export const getLocalStorage = (key: string) =>
get(key).then((value) => {
if (value === null) {
return value;
} else {
return JSON.parse(value);
}
});
export const requireLocalStorage = (key: string) =>
get(key) // `getLocalStorage()` would turn 'null' into `null`
.should('not.equal', null)
.then((value) => JSON.parse(value));
+7
View File
@@ -0,0 +1,7 @@
import { e2e } from '../index';
export function waitForMonacoToLoad() {
e2e.components.QueryField.container().children('[data-testid="Spinner"]').should('not.exist');
cy.window().its('monaco').should('exist');
cy.get('.monaco-editor textarea:first').should('exist');
}
@@ -0,0 +1,57 @@
import { DeleteDashboardConfig } from '../flows/deleteDashboard';
import { DeleteDataSourceConfig } from '../flows/deleteDataSource';
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;
}
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> =>
cy
.wrap(
{
getScenarioContext: (): ScenarioContext => ({ ...scenarioContext }),
},
{ log: false }
)
.invoke({ log: false }, 'getScenarioContext');
export const setScenarioContext = (newContext: Partial<ScenarioContext>): Cypress.Chainable<ScenarioContext> =>
cy
.wrap(
{
setScenarioContext: () => {
Object.assign(scenarioContext, newContext);
},
},
{ log: false }
)
.invoke({ log: false }, 'setScenarioContext');
+11
View File
@@ -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,
};
+136
View File
@@ -0,0 +1,136 @@
import { CssSelector, FunctionSelector, Selectors, StringSelector, UrlSelector } from '@grafana/e2e-selectors';
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<string, string | FunctionSelector | CssSelector | UrlSelector | Selectors>
? 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: unknown) => cy.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));
}
cy.logToConsole('Visiting', parsedUrl);
if (queryParams) {
return cy.visit({ url: parsedUrl, qs: queryParams });
} else {
return cy.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 cy.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('');
logOutput(selector);
return cy.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 cy.get(selector);
}
const selector = value('');
logOutput(selector);
return cy.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 cy.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 };
};
+12
View File
@@ -0,0 +1,12 @@
const getBaseUrl = () => Cypress.env('BASE_URL') || Cypress.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];
}
};