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:
Hugo Häggmark
2020-04-24 08:48:04 +02:00
committed by GitHub
parent db3f2b90e9
commit e4d492fd35
27 changed files with 471 additions and 148 deletions

View File

@@ -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;
// });

View File

@@ -1,4 +1,4 @@
'use strict'
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./index.production.js');

View 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',
},
}),
};

View File

@@ -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,
};

View 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();
};

View File

@@ -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),

View File

@@ -0,0 +1,9 @@
import { pageFactory } from '../support';
export const Explore = pageFactory({
url: '/explore',
selectors: {
container: 'Explore',
runButton: 'Run button',
},
});

View File

@@ -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',
},
}),
};

View File

@@ -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',
},
});

View File

@@ -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());
}
});

View File

@@ -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;
};

View File

@@ -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>

View File

@@ -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} />}