mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
e2e: adds inspect drawer tests (#23823)
* Explore: Create basic E2E test * Feature: adds e2e tests for panel inspector * Refactor: adds ts-ignore because of type checking errors * Refactor: changes after PR comments and updates snapshot * Refactor: adds typings back for IScope * Refactor: changes after PR comments Co-authored-by: Andreas Opferkuch <andreas.opferkuch@gmail.com>
This commit is contained in:
@@ -23,3 +23,10 @@ if (Cypress.env('SLOWMO')) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// uncomment below to prevent Cypress from failing tests when unhandled errors are thrown
|
||||
// Cypress.on('uncaught:exception', (err, runnable) => {
|
||||
// // returning false here prevents Cypress from
|
||||
// // failing the test
|
||||
// return false;
|
||||
// });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
'use strict'
|
||||
'use strict';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./index.production.js');
|
||||
|
||||
67
packages/grafana-e2e/src/components/index.ts
Normal file
67
packages/grafana-e2e/src/components/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { TestData } from '../pages/testdata';
|
||||
import { Panel } from '../pages/panel';
|
||||
import { EditPanel } from '../pages/editPanel';
|
||||
import { Graph } from '../pages/graph';
|
||||
import { componentFactory } from '../support';
|
||||
|
||||
export const Components = {
|
||||
DataSource: {
|
||||
TestData,
|
||||
},
|
||||
Panels: {
|
||||
Panel,
|
||||
EditPanel,
|
||||
Visualization: {
|
||||
Graph,
|
||||
},
|
||||
},
|
||||
Drawer: {
|
||||
General: componentFactory({
|
||||
selectors: {
|
||||
title: (title: string) => `Drawer title ${title}`,
|
||||
expand: 'Drawer expand',
|
||||
contract: 'Drawer contract',
|
||||
close: 'Drawer close',
|
||||
rcContentWrapper: () => '.drawer-content-wrapper',
|
||||
},
|
||||
}),
|
||||
},
|
||||
PanelInspector: {
|
||||
Data: componentFactory({
|
||||
selectors: {
|
||||
content: 'Panel inspector Data content',
|
||||
},
|
||||
}),
|
||||
Stats: componentFactory({
|
||||
selectors: {
|
||||
content: 'Panel inspector Stats content',
|
||||
},
|
||||
}),
|
||||
Json: componentFactory({
|
||||
selectors: {
|
||||
content: 'Panel inspector Json content',
|
||||
},
|
||||
}),
|
||||
Query: componentFactory({
|
||||
selectors: {
|
||||
content: 'Panel inspector Query content',
|
||||
},
|
||||
}),
|
||||
},
|
||||
Tab: componentFactory({
|
||||
selectors: {
|
||||
title: (title: string) => `Tab ${title}`,
|
||||
active: () => '[class*="-activeTabStyle"]',
|
||||
},
|
||||
}),
|
||||
QueryEditorToolbarItem: componentFactory({
|
||||
selectors: {
|
||||
button: (title: string) => `QueryEditor toolbar item button ${title}`,
|
||||
},
|
||||
}),
|
||||
BackButton: componentFactory({
|
||||
selectors: {
|
||||
backArrow: 'Go Back button',
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { login } from './login';
|
||||
import { openDashboard } from './openDashboard';
|
||||
import { saveDashboard } from './saveDashboard';
|
||||
import { saveNewDashboard } from './saveNewDashboard';
|
||||
import { openPanelMenuItem, PanelMenuItems } from './openPanelMenuItem';
|
||||
|
||||
export const Flows = {
|
||||
addDashboard,
|
||||
@@ -20,4 +21,6 @@ export const Flows = {
|
||||
openDashboard,
|
||||
saveDashboard,
|
||||
saveNewDashboard,
|
||||
openPanelMenuItem,
|
||||
PanelMenuItems,
|
||||
};
|
||||
|
||||
16
packages/grafana-e2e/src/flows/openPanelMenuItem.ts
Normal file
16
packages/grafana-e2e/src/flows/openPanelMenuItem.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { e2e } from '../noTypeCheck';
|
||||
|
||||
export enum PanelMenuItems {
|
||||
Edit = 'Edit',
|
||||
Inspect = 'Inspect',
|
||||
}
|
||||
|
||||
export const openPanelMenuItem = (menu: PanelMenuItems, panelTitle = 'Panel Title') => {
|
||||
e2e.components.Panels.Panel.title(panelTitle)
|
||||
.should('be.visible')
|
||||
.click();
|
||||
|
||||
e2e.components.Panels.Panel.headerItems(menu)
|
||||
.should('be.visible')
|
||||
.click();
|
||||
};
|
||||
@@ -4,15 +4,13 @@
|
||||
// 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, Components } from './pages';
|
||||
import { Pages } from './pages';
|
||||
import { Components } from './components';
|
||||
import { Flows } from './flows';
|
||||
import { getScenarioContext, setScenarioContext } from './support/scenarioContext';
|
||||
|
||||
export type SelectorFunction = (text?: string) => Cypress.Chainable<JQuery<HTMLElement>>;
|
||||
export type SelectorObject<S> = {
|
||||
visit: (args?: string) => Cypress.Chainable<Window>;
|
||||
selectors: S;
|
||||
};
|
||||
export type VisitFunction = (args?: string) => Cypress.Chainable<Window>;
|
||||
|
||||
const e2eObject = {
|
||||
env: (args: string) => Cypress.env(args),
|
||||
|
||||
9
packages/grafana-e2e/src/pages/explore.ts
Normal file
9
packages/grafana-e2e/src/pages/explore.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const Explore = pageFactory({
|
||||
url: '/explore',
|
||||
selectors: {
|
||||
container: 'Explore',
|
||||
runButton: 'Run button',
|
||||
},
|
||||
});
|
||||
@@ -8,14 +8,10 @@ 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 { Explore } from './explore';
|
||||
import { SaveDashboardModal } from './saveDashboardModal';
|
||||
import { Panel } from './panel';
|
||||
import { SharePanelModal } from './sharePanelModal';
|
||||
import { ConstantVariable, QueryVariable, VariableGeneral, Variables, VariablesSubMenu } from './variables';
|
||||
import { pageFactory } from '../support';
|
||||
|
||||
export const Pages = {
|
||||
Login,
|
||||
@@ -44,22 +40,8 @@ export const Pages = {
|
||||
SaveDashboardAsModal,
|
||||
SaveDashboardModal,
|
||||
SharePanelModal,
|
||||
};
|
||||
|
||||
export const Components = {
|
||||
DataSource: {
|
||||
TestData,
|
||||
Explore: {
|
||||
visit: () => Explore.visit(),
|
||||
General: Explore,
|
||||
},
|
||||
Panels: {
|
||||
Panel,
|
||||
EditPanel,
|
||||
Visualization: {
|
||||
Graph,
|
||||
},
|
||||
},
|
||||
BackButton: pageFactory({
|
||||
selectors: {
|
||||
backArrow: 'Go Back button',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { pageFactory } from '../../support';
|
||||
import { componentFactory } from '../../support';
|
||||
|
||||
export const QueryTab = pageFactory({
|
||||
url: '',
|
||||
export const QueryTab = componentFactory({
|
||||
selectors: {
|
||||
scenarioSelect: 'Test Data Query scenario select',
|
||||
max: 'TestData max',
|
||||
min: 'TestData min',
|
||||
noise: 'TestData noise',
|
||||
seriesCount: 'TestData series count',
|
||||
spread: 'TestData spread',
|
||||
startValue: 'TestData start value',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { e2e } from '../index';
|
||||
import { Flows } from '../flows';
|
||||
import { getScenarioContext } from './scenarioContext';
|
||||
|
||||
export interface ScenarioArguments {
|
||||
describeName: string;
|
||||
@@ -17,34 +18,48 @@ export const e2eScenario = ({
|
||||
addScenarioDataSource = false,
|
||||
addScenarioDashBoard = false,
|
||||
}: ScenarioArguments) => {
|
||||
// when we started to use import { e2e } from '@grafana/e2e'; in grafana/ui components
|
||||
// then type checking @grafana/run-time started to fail with
|
||||
// Cannot find name 'describe'. Do you need to install type definitions for a test runner? Try `npm i @types/jest` or `npm i @types/mocha`.
|
||||
// Haven't investigated deeper why this happens yet so adding ts-ignore as temporary solution
|
||||
// @todo remove `@ts-ignore` when possible
|
||||
// @ts-ignore
|
||||
describe(describeName, () => {
|
||||
if (skipScenario) {
|
||||
// @todo remove `@ts-ignore` when possible
|
||||
// @ts-ignore
|
||||
it.skip(itName, () => scenario());
|
||||
} else {
|
||||
// @todo remove `@ts-ignore` when possible
|
||||
// @ts-ignore
|
||||
beforeEach(() => {
|
||||
e2e.flows.login('admin', 'admin');
|
||||
Flows.login('admin', 'admin');
|
||||
if (addScenarioDataSource) {
|
||||
e2e.flows.addDataSource();
|
||||
Flows.addDataSource();
|
||||
}
|
||||
if (addScenarioDashBoard) {
|
||||
e2e.flows.addDashboard();
|
||||
Flows.addDashboard();
|
||||
}
|
||||
});
|
||||
|
||||
// @todo remove `@ts-ignore` when possible
|
||||
// @ts-ignore
|
||||
afterEach(() => {
|
||||
// @todo remove `@ts-ignore` when possible
|
||||
// @ts-ignore
|
||||
e2e.getScenarioContext().then(({ lastAddedDashboardUid, lastAddedDataSource }) => {
|
||||
getScenarioContext().then(({ lastAddedDashboardUid, lastAddedDataSource }) => {
|
||||
if (lastAddedDataSource) {
|
||||
e2e.flows.deleteDataSource(lastAddedDataSource);
|
||||
Flows.deleteDataSource(lastAddedDataSource);
|
||||
}
|
||||
|
||||
if (lastAddedDashboardUid) {
|
||||
e2e.flows.deleteDashboard(lastAddedDashboardUid);
|
||||
Flows.deleteDashboard(lastAddedDashboardUid);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// @todo remove `@ts-ignore` when possible
|
||||
// @ts-ignore
|
||||
it(itName, () => scenario());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { Selector } from './selector';
|
||||
import { fromBaseUrl } from './url';
|
||||
import { e2e } from '../index';
|
||||
import { SelectorFunction, SelectorObject } from '../noTypeCheck';
|
||||
import { SelectorFunction, VisitFunction } 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 | Function;
|
||||
export type SelectorFunctions<S> = { [P in keyof S]: SelectorFunction };
|
||||
|
||||
export type Page<S> = SelectorFunctions<S> & {
|
||||
selectors: S;
|
||||
visit: VisitFunction;
|
||||
};
|
||||
export interface PageFactoryArgs<S> {
|
||||
selectors: S;
|
||||
url?: string | Function;
|
||||
}
|
||||
|
||||
export const pageFactory = <S extends Selectors>({ url, selectors }: PageFactoryArgs<S>): PageFactory<S> => {
|
||||
export const pageFactory = <S extends Selectors>({ url, selectors }: PageFactoryArgs<S>): Page<S> => {
|
||||
const visit = (args?: string) => {
|
||||
if (!url) {
|
||||
return e2e().visit('');
|
||||
@@ -29,7 +33,7 @@ export const pageFactory = <S extends Selectors>({ url, selectors }: PageFactory
|
||||
e2e().logToConsole('Visiting', parsedUrl);
|
||||
return e2e().visit(parsedUrl);
|
||||
};
|
||||
const pageObjects: PageObjects<S> = {} as PageObjects<S>;
|
||||
const pageObjects: SelectorFunctions<S> = {} as SelectorFunctions<S>;
|
||||
const keys = Object.keys(selectors);
|
||||
|
||||
keys.forEach(key => {
|
||||
@@ -62,3 +66,11 @@ export const pageFactory = <S extends Selectors>({ url, selectors }: PageFactory
|
||||
selectors,
|
||||
};
|
||||
};
|
||||
|
||||
type Component<S> = Omit<Page<S>, 'visit'>;
|
||||
type ComponentFactoryArgs<S> = Omit<PageFactoryArgs<S>, 'url'>;
|
||||
|
||||
export const componentFactory = <S extends Selectors>(args: ComponentFactoryArgs<S>): Component<S> => {
|
||||
const { visit, ...rest } = pageFactory(args);
|
||||
return rest;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import { css } from 'emotion';
|
||||
import CustomScrollbar from '../CustomScrollbar/CustomScrollbar';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
import { stylesFactory, useTheme } from '../../themes';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
export interface Props {
|
||||
children: ReactNode;
|
||||
@@ -93,17 +94,40 @@ export const Drawer: FC<Props> = ({
|
||||
getContainer={inline ? false : 'body'}
|
||||
style={{ position: `${inline && 'absolute'}` } as CSSProperties}
|
||||
className={drawerStyles.drawer}
|
||||
aria-label={
|
||||
typeof title === 'string'
|
||||
? e2e.components.Drawer.General.selectors.title(title)
|
||||
: e2e.components.Drawer.General.selectors.title('no title')
|
||||
}
|
||||
>
|
||||
{typeof title === 'string' && (
|
||||
<div className={drawerStyles.header}>
|
||||
<div className={drawerStyles.actions}>
|
||||
{expandable && !isExpanded && (
|
||||
<IconButton name="angle-left" size="xl" onClick={() => setIsExpanded(true)} surface="header" />
|
||||
<IconButton
|
||||
name="angle-left"
|
||||
size="xl"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
surface="header"
|
||||
aria-label={e2e.components.Drawer.General.selectors.expand}
|
||||
/>
|
||||
)}
|
||||
{expandable && isExpanded && (
|
||||
<IconButton name="angle-right" size="xl" onClick={() => setIsExpanded(false)} surface="header" />
|
||||
<IconButton
|
||||
name="angle-right"
|
||||
size="xl"
|
||||
onClick={() => setIsExpanded(false)}
|
||||
surface="header"
|
||||
aria-label={e2e.components.Drawer.General.selectors.contract}
|
||||
/>
|
||||
)}
|
||||
<IconButton name="times" size="xl" onClick={onClose} surface="header" />
|
||||
<IconButton
|
||||
name="times"
|
||||
size="xl"
|
||||
onClick={onClose}
|
||||
surface="header"
|
||||
aria-label={e2e.components.Drawer.General.selectors.close}
|
||||
/>
|
||||
</div>
|
||||
<div className={drawerStyles.titleWrapper}>
|
||||
<h3>{title}</h3>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Icon } from '../Icon/Icon';
|
||||
import { IconName } from '../../types';
|
||||
import { stylesFactory, useTheme } from '../../themes';
|
||||
import { Counter } from './Counter';
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
export interface TabProps {
|
||||
label: string;
|
||||
@@ -40,6 +41,7 @@ const getTabStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
}
|
||||
`,
|
||||
activeStyle: css`
|
||||
label: activeTabStyle;
|
||||
border-color: ${theme.palette.orange} ${colors.pageHeaderBorder} transparent;
|
||||
background: ${colors.bodyBg};
|
||||
color: ${colors.link};
|
||||
@@ -64,7 +66,11 @@ export const Tab: FC<TabProps> = ({ label, active, icon, onChangeTab, counter })
|
||||
const tabsStyles = getTabStyles(theme);
|
||||
|
||||
return (
|
||||
<li className={cx(tabsStyles.tabItem, active && tabsStyles.activeStyle)} onClick={onChangeTab}>
|
||||
<li
|
||||
className={cx(tabsStyles.tabItem, active && tabsStyles.activeStyle)}
|
||||
onClick={onChangeTab}
|
||||
aria-label={e2e.components.Tab.selectors.title(label)}
|
||||
>
|
||||
{icon && <Icon name={icon} />}
|
||||
{label}
|
||||
{typeof counter === 'number' && <Counter value={counter} />}
|
||||
|
||||
Reference in New Issue
Block a user