E2E: Adds tests for QueryVariable CRUD (#20448)

* WIP: Adds basic template variables CRUD start

* e2eTests: Adds aria-labels in VariableEditorCtrl

* Refactor: Simplifies a bit

* e2eTests: Adds first Template Variable CRUD for QueryVariable

* Tests: Adds ArrayPageOjbectType

* Tests: Adds createQueryVariable method

* Tests: Refactor CRUD test

* Tests: Adds datasource and dashboard to scenario

* Refactor: Fixes type errors

* Refactor: Move pages to toolkit
This commit is contained in:
Hugo Häggmark 2019-11-25 07:29:01 +01:00 committed by GitHub
parent 31f4dea3d0
commit 2c2ed8371d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 910 additions and 317 deletions

View File

@ -1,6 +1,6 @@
import { Page } from 'puppeteer-core';
import { constants } from './constants';
import { PageObject } from './pageObjects';
import { PageObject, Selector } from './pageObjects';
export interface ExpectSelectorConfig {
selector: string;
@ -22,22 +22,34 @@ export interface TestPageType<T> {
}
type PageObjects<T> = { [P in keyof T]: T[P] };
type SelectorFunc = () => string;
export interface TestPageConfig<T> {
url?: string;
pageObjects: PageObjects<T>;
pageObjects: { [P in keyof T]: string | SelectorFunc };
}
export class TestPage<T> implements TestPageType<T> {
pageObjects: PageObjects<T>;
private page?: Page;
private pageUrl?: string;
private readonly pageUrl?: string;
constructor(config: TestPageConfig<T>) {
if (config.url) {
this.pageUrl = `${constants.baseUrl}${config.url}`;
}
this.pageObjects = config.pageObjects;
this.pageObjects = {} as PageObjects<T>;
Object.keys(config.pageObjects).map(async key => {
const selector = (config.pageObjects as any)[key];
if (typeof selector === 'function') {
(this.pageObjects as any)[key] = new PageObject(selector());
}
if (typeof selector === 'string') {
(this.pageObjects as any)[key] = new PageObject(Selector.fromAriaLabel(selector));
}
});
}
init = async (page: Page): Promise<void> => {

View File

@ -5,6 +5,10 @@ export class Selector {
return `[aria-label="${selector}"]`;
};
static fromSwitchLabel = (selector: string) => {
return `${Selector.fromAriaLabel(selector)} .gf-form-switch input`;
};
static fromSelector = (selector: string) => {
return selector;
};
@ -14,6 +18,7 @@ export interface PageObjectType {
init: (page: Page) => Promise<void>;
exists: () => Promise<void>;
containsText: (text: string) => Promise<void>;
waitForSelector: (timeoutInMs?: number) => Promise<void>;
}
export interface ClickablePageObjectType extends PageObjectType {
@ -22,13 +27,36 @@ export interface ClickablePageObjectType extends PageObjectType {
export interface InputPageObjectType extends PageObjectType {
enter: (text: string) => Promise<void>;
containsPlaceholder: (text: string) => Promise<void>;
blur: () => Promise<void>;
}
export interface SelectPageObjectType extends PageObjectType {
select: (text: string) => Promise<void>;
selectedTextIs: (text: string) => Promise<void>;
}
export class PageObject implements PageObjectType {
export interface SwitchPageObjectType extends PageObjectType {
toggle: () => Promise<void>;
isSwitchedOn: () => Promise<void>;
isSwitchedOff: () => Promise<void>;
}
export interface ArrayPageObjectType {
hasLength: (length: number) => Promise<void>;
clickAtPos: (index: number) => Promise<void>;
containsTextAtPos: (text: string, index: number) => Promise<void>;
waitForSelector: (timeoutInMs?: number) => Promise<void>;
}
export class PageObject
implements
PageObjectType,
ClickablePageObjectType,
InputPageObjectType,
SelectPageObjectType,
SwitchPageObjectType,
ArrayPageObjectType {
protected page?: Page;
constructor(protected selector: string) {}
@ -38,50 +66,109 @@ export class PageObject implements PageObjectType {
};
exists = async (): Promise<void> => {
console.log('Checking for existence of:', this.selector);
const options = { visible: true } as any;
await expect(this.page).not.toBeNull();
await expect(this.page).toMatchElement(this.selector, options);
};
containsText = async (text: string): Promise<void> => {
console.log(`Checking for existence of '${text}' for:`, this.selector);
const options = { visible: true, text } as any;
await expect(this.page).not.toBeNull();
await expect(this.page).toMatchElement(this.selector, options);
};
}
export class ClickablePageObject extends PageObject implements ClickablePageObjectType {
constructor(selector: string) {
super(selector);
}
containsPlaceholder = async (expectedPlaceholder: string): Promise<void> => {
console.log(`Checking for placeholder '${expectedPlaceholder}' in:`, this.selector);
await expect(this.page).not.toBeNull();
const placeholder = await this.page!.$eval(this.selector, (input: any) => input.placeholder);
await expect(placeholder).toEqual(expectedPlaceholder);
};
hasLength = async (length: number): Promise<void> => {
console.log('Checking for length of', this.selector);
const result = await this.page!.$$eval(this.selector, elements => elements.length);
await expect(result).toEqual(length);
};
containsTextAtPos = async (text: string, index: number): Promise<void> => {
console.log(`Checking for text ${text} at position ${index} of`, this.selector);
await expect(this.page).not.toBeNull();
const result = await this.page!.$$eval(this.selector, elements => elements.map((el: any) => el.innerText));
await expect(result[index]!.trim()).toEqual(text);
};
click = async (): Promise<void> => {
console.log('Trying to click on:', this.selector);
await expect(this.page).not.toBeNull();
await expect(this.page).toClick(this.selector);
};
}
export class InputPageObject extends PageObject implements InputPageObjectType {
constructor(selector: string) {
super(selector);
}
clickAtPos = async (index: number): Promise<void> => {
console.log(`Trying to clicking at position:${index} on:`, this.selector);
await expect(this.page).not.toBeNull();
const elements = await this.page!.$$(this.selector);
const element = await elements[index];
await element.click();
};
toggle = async (): Promise<void> => {
const switchSelector = this.selector.replace(' .gf-form-switch input', '');
console.log('Trying to toggle:', switchSelector);
await expect(this.page).not.toBeNull();
await expect(this.page).toClick(switchSelector);
};
enter = async (text: string): Promise<void> => {
console.log(`Trying to enter text:${text} into:`, this.selector);
await expect(this.page).not.toBeNull();
await expect(this.page).toFill(this.selector, text);
};
}
export class SelectPageObject extends PageObject implements SelectPageObjectType {
constructor(selector: string) {
super(selector);
}
select = async (text: string): Promise<void> => {
console.log(`Trying to select text:${text} in dropdown:`, this.selector);
await expect(this.page).not.toBeNull();
await this.page!.select(this.selector, text);
};
selectedTextIs = async (text: string): Promise<void> => {
console.log(`Trying to get selected text from dropdown:`, this.selector);
await expect(this.page).not.toBeNull();
const selectedText = await this.page!.$eval(this.selector, (select: any) => {
if (select.selectedIndex === -1) {
return '';
}
return select.options[select.selectedIndex].innerText;
});
await expect(selectedText).toEqual(text);
};
waitForSelector = async (timeoutInMs?: number): Promise<void> => {
console.log('Waiting for', this.selector);
await expect(this.page).not.toBeNull();
await this.page!.waitForSelector(this.selector, { timeout: timeoutInMs || 1000 });
};
isSwitchedOn = async (): Promise<void> => {
const checked = await this.getChecked();
await expect(checked).toBe(true);
};
isSwitchedOff = async (): Promise<void> => {
const checked = await this.getChecked();
await expect(checked).toBe(false);
};
blur = async (): Promise<void> => {
console.log('Trying to blur:', this.selector);
await expect(this.page).not.toBeNull();
await this.page!.$eval(this.selector, (input: any) => input.blur());
};
private getChecked = async (): Promise<boolean> => {
console.log('Trying get switch status for:', this.selector);
await expect(this.page).not.toBeNull();
return await this.page!.$eval(this.selector, (input: any) => input.checked);
};
}

View File

@ -0,0 +1,55 @@
import { Page } from 'puppeteer-core';
import { ClickablePageObjectType } from '../../pageObjects';
import { TestPage } from '../../pageInfo';
import { dashboardPage } from './dashboardPage';
import { dashboardSettingsPage } from './dashboardSettingsPage';
import { saveDashboardModal } from './saveDashboardModal';
import { dashboardsPageFactory } from './dashboardsPage';
import { confirmModal } from '../modals/confirmModal';
export interface CreateDashboardPage {
addQuery: ClickablePageObjectType;
saveDashboard: ClickablePageObjectType;
}
export const createDashboardPage = new TestPage<CreateDashboardPage>({
url: '/dashboard/new',
pageObjects: {
addQuery: 'Add Query CTA button',
saveDashboard: 'Save dashboard navbar button',
},
});
export const createEmptyDashboardPage = async (page: Page, dashboardTitle: string) => {
await createDashboardPage.init(page);
await createDashboardPage.navigateTo();
await createDashboardPage.pageObjects.saveDashboard.click();
await saveDashboardModal.init(page);
await saveDashboardModal.expectSelector({ selector: 'save-dashboard-as-modal' });
await saveDashboardModal.pageObjects.name.enter(dashboardTitle);
await saveDashboardModal.pageObjects.save.click();
await saveDashboardModal.pageObjects.success.exists();
await dashboardPage.init(page);
return dashboardPage;
};
export const cleanDashboard = async (page: Page, dashboardTitle: string) => {
const dashboardsPage = dashboardsPageFactory(dashboardTitle);
await dashboardsPage.init(page);
await dashboardsPage.navigateTo();
await dashboardsPage.pageObjects.dashboard.exists();
await dashboardsPage.pageObjects.dashboard.click();
await dashboardPage.init(page);
await dashboardPage.pageObjects.settings.click();
await dashboardSettingsPage.init(page);
await dashboardSettingsPage.pageObjects.deleteDashBoard.click();
await confirmModal.init(page);
await confirmModal.pageObjects.delete.click();
await confirmModal.pageObjects.success.exists();
};

View File

@ -0,0 +1,49 @@
import { ArrayPageObjectType, ClickablePageObjectType, PageObjectType } from '../../pageObjects';
import { TestPage } from '../../pageInfo';
export interface DashboardPage {
settings: ClickablePageObjectType;
submenuItemLabel: ArrayPageObjectType;
submenuItemValueDropDownValueLink: ArrayPageObjectType;
submenuItemValueDropDownDropDown: PageObjectType;
submenuItemValueDropDownSelectedLink: PageObjectType;
submenuItemValueDropDownOptionText: ArrayPageObjectType;
}
export const dashboardPage = new TestPage<DashboardPage>({
pageObjects: {
settings: 'Dashboard settings navbar button',
submenuItemLabel: 'Dashboard template variables submenu LabelName label',
submenuItemValueDropDownValueLink: 'Dashboard template variables Variable Value DropDown value link',
submenuItemValueDropDownDropDown: 'Dashboard template variables Variable Value DropDown DropDown',
submenuItemValueDropDownSelectedLink: 'Dashboard template variables Variable Value DropDown Selected link',
submenuItemValueDropDownOptionText: 'Dashboard template variables Variable Value DropDown option text',
},
});
export interface AssertVariableLabelsAndComponentsArguments {
label: string;
options: string[];
}
export const assertVariableLabelsAndComponents = async (
page: TestPage<DashboardPage>,
args: AssertVariableLabelsAndComponentsArguments[]
) => {
console.log('Asserting variable components and labels');
await page.pageObjects.submenuItemLabel.waitForSelector();
await page.pageObjects.submenuItemLabel.hasLength(args.length);
await page.pageObjects.submenuItemValueDropDownValueLink.hasLength(args.length);
for (let index = 0; index < args.length; index++) {
const { label, options } = args[index];
await page.pageObjects.submenuItemLabel.containsTextAtPos(label, index);
await page.pageObjects.submenuItemValueDropDownValueLink.containsTextAtPos(options[1], index);
await page.pageObjects.submenuItemValueDropDownValueLink.clickAtPos(index);
await page.pageObjects.submenuItemValueDropDownOptionText.hasLength(options.length);
for (let optionIndex = 0; optionIndex < options.length; optionIndex++) {
await page.pageObjects.submenuItemValueDropDownOptionText.containsTextAtPos(options[optionIndex], optionIndex);
}
}
console.log('Asserting variable components and labels, Ok');
};

View File

@ -0,0 +1,16 @@
import { ClickablePageObjectType } from '../../pageObjects';
import { TestPage } from '../../pageInfo';
export interface DashboardSettingsPage {
deleteDashBoard: ClickablePageObjectType;
variablesSection: ClickablePageObjectType;
saveDashBoard: ClickablePageObjectType;
}
export const dashboardSettingsPage = new TestPage<DashboardSettingsPage>({
pageObjects: {
deleteDashBoard: 'Dashboard settings page delete dashboard button',
variablesSection: 'Dashboard settings section Variables',
saveDashBoard: 'Dashboard settings aside actions Save button',
},
});

View File

@ -1,4 +1,5 @@
import { TestPage, ClickablePageObjectType, ClickablePageObject, Selector } from '@grafana/toolkit/src/e2e';
import { ClickablePageObjectType } from '../../pageObjects';
import { TestPage } from '../../pageInfo';
export interface DashboardsPage {
dashboard: ClickablePageObjectType;
@ -8,6 +9,6 @@ export const dashboardsPageFactory = (dashboardTitle: string) =>
new TestPage<DashboardsPage>({
url: '/dashboards',
pageObjects: {
dashboard: new ClickablePageObject(Selector.fromAriaLabel(dashboardTitle)),
dashboard: dashboardTitle,
},
});

View File

@ -0,0 +1,14 @@
import { ClickablePageObjectType, PageObject, Selector } from '../../pageObjects';
import { TestPage } from '../../pageInfo';
export interface SaveChangesDashboardModal {
save: ClickablePageObjectType;
success: PageObject;
}
export const saveChangesDashboardModal = new TestPage<SaveChangesDashboardModal>({
pageObjects: {
save: 'Dashboard settings Save Dashboard Modal Save button',
success: () => Selector.fromSelector('.alert-success'),
},
});

View File

@ -0,0 +1,16 @@
import { ClickablePageObjectType, InputPageObjectType, PageObject, Selector } from '../../pageObjects';
import { TestPage } from '../../pageInfo';
export interface SaveDashboardModal {
name: InputPageObjectType;
save: ClickablePageObjectType;
success: PageObject;
}
export const saveDashboardModal = new TestPage<SaveDashboardModal>({
pageObjects: {
name: 'Save dashboard title field',
save: 'Save dashboard button',
success: () => Selector.fromSelector('.alert-success'),
},
});

View File

@ -0,0 +1,13 @@
import { ClickablePageObjectType } from '../../pageObjects';
import { TestPage } from '../../pageInfo';
export interface AddDataSourcePage {
testDataDB: ClickablePageObjectType;
}
export const addDataSourcePage = new TestPage<AddDataSourcePage>({
url: '/datasources/new',
pageObjects: {
testDataDB: 'TestData DB datasource plugin',
},
});

View File

@ -0,0 +1,62 @@
import { Page } from 'puppeteer-core';
import { ClickablePageObjectType } from '../../pageObjects';
import { TestPage } from '../../pageInfo';
import { editDataSourcePage } from './editDataSourcePage';
import { addDataSourcePage } from './addDataSourcePage';
import { confirmModal } from '../modals/confirmModal';
export interface DataSourcesPage {
testData: ClickablePageObjectType;
}
export const dataSourcesPageFactory = (testDataSourceName: string) =>
new TestPage<DataSourcesPage>({
url: '/datasources',
pageObjects: {
testData: `Data source list item for ${testDataSourceName}`,
},
});
export const addTestDataSourceAndVerify = async (page: Page) => {
// Add TestData DB
const testDataSourceName = `e2e - TestData-${new Date().getTime()}`;
await addDataSourcePage.init(page);
await addDataSourcePage.navigateTo();
await addDataSourcePage.pageObjects.testDataDB.exists();
await addDataSourcePage.pageObjects.testDataDB.click();
await editDataSourcePage.init(page);
await editDataSourcePage.waitForNavigation();
await editDataSourcePage.pageObjects.name.enter(testDataSourceName);
await editDataSourcePage.pageObjects.saveAndTest.click();
await editDataSourcePage.pageObjects.alert.exists();
await editDataSourcePage.pageObjects.alertMessage.containsText('Data source is working');
// Verify that data source is listed
const url = await editDataSourcePage.getUrlWithoutBaseUrl();
const expectedUrl = url.substring(1, url.length - 1);
const selector = `a[href="${expectedUrl}"]`;
const dataSourcesPage = dataSourcesPageFactory(testDataSourceName);
await dataSourcesPage.init(page);
await dataSourcesPage.navigateTo();
await dataSourcesPage.expectSelector({ selector });
return testDataSourceName;
};
export const cleanUpTestDataSource = async (page: Page, testDataSourceName: string) => {
const dataSourcesPage = dataSourcesPageFactory(testDataSourceName);
await dataSourcesPage.init(page);
await dataSourcesPage.navigateTo();
await dataSourcesPage.pageObjects.testData.click();
await editDataSourcePage.init(page);
await editDataSourcePage.pageObjects.delete.exists();
await editDataSourcePage.pageObjects.delete.click();
await confirmModal.init(page);
await confirmModal.pageObjects.delete.click();
await confirmModal.pageObjects.success.exists();
};

View File

@ -0,0 +1,20 @@
import { ClickablePageObjectType, InputPageObjectType, PageObjectType } from '../../pageObjects';
import { TestPage } from '../../pageInfo';
export interface EditDataSourcePage {
name: InputPageObjectType;
delete: ClickablePageObjectType;
saveAndTest: ClickablePageObjectType;
alert: PageObjectType;
alertMessage: PageObjectType;
}
export const editDataSourcePage = new TestPage<EditDataSourcePage>({
pageObjects: {
name: 'Datasource settings page name input field',
delete: 'Delete button',
saveAndTest: 'Save and Test button',
alert: 'Datasource settings page Alert',
alertMessage: 'Datasource settings page Alert message',
},
});

View File

@ -1,2 +1,12 @@
export * from './loginPage';
export * from './pluginsPage';
export * from './dashboards/createDashboardPage';
export * from './dashboards/dashboardPage';
export * from './dashboards/dashboardSettingsPage';
export * from './dashboards/dashboardsPage';
export * from './dashboards/saveChangesDashboardModal';
export * from './dashboards/saveDashboardModal';
export * from './datasources/addDataSourcePage';
export * from './datasources/dataSources';
export * from './datasources/editDataSourcePage';
export * from './modals/confirmModal';

View File

@ -1,11 +1,5 @@
import { TestPage } from '../pageInfo';
import {
Selector,
InputPageObject,
InputPageObjectType,
ClickablePageObjectType,
ClickablePageObject,
} from '../pageObjects';
import { ClickablePageObjectType, InputPageObjectType } from '../pageObjects';
export interface LoginPage {
username: InputPageObjectType;
@ -16,8 +10,8 @@ export interface LoginPage {
export const loginPage = new TestPage<LoginPage>({
url: '/login',
pageObjects: {
username: new InputPageObject(Selector.fromAriaLabel('Username input field')),
password: new InputPageObject(Selector.fromAriaLabel('Password input field')),
submit: new ClickablePageObject(Selector.fromAriaLabel('Login button')),
username: 'Username input field',
password: 'Password input field',
submit: 'Login button',
},
});

View File

@ -0,0 +1,14 @@
import { ClickablePageObjectType, PageObject, Selector } from '../../pageObjects';
import { TestPage } from '../../pageInfo';
export interface ConfirmModal {
delete: ClickablePageObjectType;
success: PageObject;
}
export const confirmModal = new TestPage<ConfirmModal>({
pageObjects: {
delete: 'Confirm Modal Danger Button',
success: () => Selector.fromSelector('.alert-success'),
},
});

View File

@ -1,30 +1,69 @@
import { Browser, Page } from 'puppeteer-core';
import { launchBrowser } from './launcher';
import { ensureLoggedIn } from './login';
import { cleanDashboard, createEmptyDashboardPage } from './pages/dashboards/createDashboardPage';
import { DashboardPage } from './pages/dashboards/dashboardPage';
import { TestPage } from './pageInfo';
import { addTestDataSourceAndVerify, cleanUpTestDataSource } from './pages/datasources/dataSources';
export interface ScenarioArguments {
describeName: string;
itName: string;
scenario: (browser: Browser, page: Page, datasourceName?: string, dashboardPage?: TestPage<DashboardPage>) => void;
skipScenario?: boolean;
createTestDataSource?: boolean;
createTestDashboard?: boolean;
}
export const e2eScenario = ({
describeName,
itName,
scenario,
skipScenario = false,
createTestDataSource = false,
createTestDashboard = false,
}: ScenarioArguments) => {
describe(describeName, () => {
if (skipScenario) {
it.skip(itName, async () => {
expect(false).toBe(true);
});
return;
}
export const e2eScenario = (
title: string,
testDescription: string,
callback: (browser: Browser, page: Page) => void
) => {
describe(title, () => {
let browser: Browser;
let page: Page;
let testDataSourceName: string;
let testDashboardTitle: string;
let dashboardPage: TestPage<DashboardPage>;
beforeAll(async () => {
browser = await launchBrowser();
page = await browser.newPage();
await ensureLoggedIn(page);
if (createTestDataSource) {
testDataSourceName = await addTestDataSourceAndVerify(page);
}
if (createTestDashboard) {
testDashboardTitle = `e2e - ${new Date().getTime()}`;
dashboardPage = await createEmptyDashboardPage(page, testDashboardTitle);
}
});
afterAll(async () => {
if (testDataSourceName) {
await cleanUpTestDataSource(page, testDataSourceName);
}
if (testDashboardTitle && dashboardPage) {
await cleanDashboard(page, testDashboardTitle);
}
if (browser) {
await browser.close();
}
});
it(testDescription, async () => {
await callback(browser, page);
it(itName, async () => {
await scenario(browser, page, testDataSourceName, dashboardPage);
});
});
};

View File

@ -1,6 +1,6 @@
import { Browser, Page } from 'puppeteer-core';
import { e2eScenario, takeScreenShot, pages } from '@grafana/toolkit/src/e2e';
import { e2eScenario, pages, takeScreenShot } from '@grafana/toolkit/src/e2e';
import { getEndToEndSettings } from '@grafana/toolkit/src/plugins';
// ****************************************************************
@ -11,14 +11,18 @@ const sleep = (milliseconds: number) => {
return new Promise(resolve => setTimeout(resolve, milliseconds));
};
e2eScenario('Common Plugin Test', 'should pass', async (browser: Browser, page: Page) => {
const settings = getEndToEndSettings();
const pluginPage = pages.getPluginPage(settings.plugin.id);
await pluginPage.init(page);
await pluginPage.navigateTo();
// TODO: find a better way to avoid the 'loading' page
await sleep(500);
e2eScenario({
describeName: 'Common Plugin Test',
itName: 'should pass',
scenario: async (browser: Browser, page: Page) => {
const settings = getEndToEndSettings();
const pluginPage = pages.getPluginPage(settings.plugin.id);
await pluginPage.init(page);
await pluginPage.navigateTo();
// TODO: find a better way to avoid the 'loading' page
await sleep(500);
const fileName = 'plugin-page';
await takeScreenShot(page, fileName);
const fileName = 'plugin-page';
await takeScreenShot(page, fileName);
},
});

View File

@ -1,6 +1,7 @@
import React, { useContext, MouseEvent } from 'react';
import React, { MouseEvent, useContext } from 'react';
import { CallToActionCard, LinkButton, ThemeContext } from '@grafana/ui';
import { css } from 'emotion';
export interface Props {
title: string;
buttonIcon: string;
@ -72,7 +73,14 @@ const EmptyListCTA: React.FunctionComponent<Props> = ({
: '';
const ctaElement = (
<LinkButton size="lg" onClick={onClick} href={buttonLink} icon={buttonIcon} className={ctaElementClassName}>
<LinkButton
size="lg"
onClick={onClick}
href={buttonLink}
icon={buttonIcon}
className={ctaElementClassName}
aria-label={`Call to action button ${buttonTitle}`}
>
{buttonTitle}
</LinkButton>
);

View File

@ -1,22 +1,18 @@
// Libaries
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
// Utils & Services
import { appEvents } from 'app/core/app_events';
import { PlaylistSrv } from 'app/features/playlist/playlist_srv';
// Components
import { DashNavButton } from './DashNavButton';
import { DashNavTimeControls } from './DashNavTimeControls';
import { Tooltip } from '@grafana/ui';
// State
import { updateLocation } from 'app/core/actions';
// Types
import { DashboardModel } from '../../state';
import { StoreState, CoreEvents } from 'app/types';
import { CoreEvents, StoreState } from 'app/types';
export interface OwnProps {
dashboard: DashboardModel;
@ -160,7 +156,11 @@ export class DashNav extends PureComponent<Props> {
return (
<div className="navbar-edit">
<Tooltip content="Go back (Esc)">
<button className="navbar-edit__back-btn" onClick={this.onClose}>
<button
className="navbar-edit__back-btn"
onClick={this.onClose}
aria-label="Dashboard settings Go Back button"
>
<i className="fa fa-arrow-left" />
</button>
</Tooltip>

View File

@ -1,14 +1,24 @@
<aside class="dashboard-settings__aside">
<a href="{{::section.url}}" class="dashboard-settings__nav-item" ng-class="{active: ctrl.viewId === section.id}" ng-repeat="section in ctrl.sections">
<a href="{{::section.url}}" class="dashboard-settings__nav-item" ng-class="{active: ctrl.viewId === section.id}" ng-repeat="section in ctrl.sections" aria-label="{{'Dashboard settings section ' + section.title}}">
<i class="{{::section.icon}}"></i>
{{::section.title}}
</a>
<div class="dashboard-settings__aside-actions">
<button class="btn btn-primary" ng-click="ctrl.saveDashboard()" ng-show="ctrl.canSave">
<button
class="btn btn-primary"
ng-click="ctrl.saveDashboard()"
ng-show="ctrl.canSave"
aria-label="Dashboard settings aside actions Save button"
>
Save
</button>
<button class="btn btn-inverse" ng-click="ctrl.openSaveAsModal()" ng-show="ctrl.canSaveAs">
<button
class="btn btn-inverse"
ng-click="ctrl.openSaveAsModal()"
ng-show="ctrl.canSaveAs"
aria-label="Dashboard settings aside actions Save As button"
>
Save As...
</button>
</div>

View File

@ -57,6 +57,7 @@ const template = `
class="btn btn-primary"
ng-class="{'btn-primary--processing': ctrl.isSaving}"
ng-disabled="ctrl.saveForm.$invalid || ctrl.isSaving"
aria-label="Dashboard settings Save Dashboard Modal Save button"
>
<span ng-if="!ctrl.isSaving">Save</span>
<span ng-if="ctrl.isSaving === true">Saving...</span>

View File

@ -1,9 +1,11 @@
<div class="submenu-controls">
<div ng-repeat="variable in ctrl.variables" ng-hide="variable.hide === 2" class="submenu-item gf-form-inline">
<div class="gf-form">
<label class="gf-form-label template-variable" ng-hide="variable.hide === 1">
{{variable.label || variable.name}}
</label>
<label
class="gf-form-label template-variable"
ng-hide="variable.hide === 1"
aria-label="Dashboard template variables submenu LabelName label"
>{{variable.label || variable.name}}</label>
<value-select-dropdown ng-if="variable.type !== 'adhoc' && variable.type !== 'textbox'" dashboard="ctrl.dashboard" variable="variable" on-updated="ctrl.variableUpdated(variable)"></value-select-dropdown>
<input type="text" ng-if="variable.type === 'textbox'" ng-model="variable.query" class="gf-form-input width-12" ng-blur="variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);" ng-keydown="$event.keyCode === 13 && variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);" ></input>
</div>

View File

@ -37,6 +37,7 @@ export default class DefaultVariableQueryEditor extends PureComponent<VariableQu
onBlur={this.onBlur}
placeholder="metric name or tags query"
required
aria-label="Variable editor Form Default Variable Query Editor textarea"
/>
</div>
);

View File

@ -1,9 +1,13 @@
<div ng-controller="VariableEditorCtrl" ng-init="init()">
<div class="page-action-bar">
<h3 class="dashboard-settings__header">
<a ng-click="setMode('list')">Variables</a>
<span ng-show="mode === 'new'"><i class="fa fa-fw fa-chevron-right"></i> New</span>
<span ng-show="mode === 'edit'"><i class="fa fa-fw fa-chevron-right"></i> Edit</span>
<a ng-click="setMode('list')" aria-label="Variable editor Header link">Variables</a>
<span ng-show="mode === 'new'"
><i class="fa fa-fw fa-chevron-right" aria-label="Variable editor Header mode New"></i> New</span
>
<span ng-show="mode === 'edit'"
><i class="fa fa-fw fa-chevron-right" aria-label="Variable editor Header mode Edit"></i> Edit</span
>
</h3>
<div class="page-action-bar__spacer"></div>
@ -12,25 +16,27 @@
class="btn btn-primary"
ng-click="setMode('new');"
ng-if="variables.length > 0"
ng-hide="mode === 'edit' || mode === 'new'">
New
</a>
ng-hide="mode === 'edit' || mode === 'new'"
aria-label="Variable editor New variable button"
>
New
</a>
</div>
<div ng-if="mode === 'list'">
<div ng-if="variables.length === 0">
<empty-list-cta
<empty-list-cta
on-click="setNewMode"
title="emptyListCta.title"
infoBox="emptyListCta.infoBox"
infoBoxTitle="emptyListCta.infoBoxTitle"
buttonTitle="emptyListCta.buttonTitle"
buttonIcon="emptyListCta.buttonIcon"
/>
/>
</div>
<div ng-if="variables.length">
<table class="filter-table filter-table--hover">
<table class="filter-table filter-table--hover" aria-label="Variable editor Table">
<thead>
<tr>
<th>Variable</th>
@ -41,24 +47,53 @@
<tbody>
<tr ng-repeat="variable in variables">
<td style="width: 1%">
<span ng-click="edit(variable)" class="pointer template-variable"> ${{ variable.name }} </span>
<span
ng-click="edit(variable)"
class="pointer template-variable"
aria-label="Variable editor Table Name field"
>
${{ variable.name }}
</span>
</td>
<td style="max-width: 200px;" ng-click="edit(variable)" class="pointer max-width">
<td
style="max-width: 200px;"
ng-click="edit(variable)"
class="pointer max-width"
aria-label="Variable editor Table Definition field"
>
{{ variable.definition ? variable.definition : variable.query }}
</td>
<td style="width: 1%">
<i ng-click="_.move(variables,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i>
<i
ng-click="_.move(variables,$index,$index-1)"
ng-hide="$first"
class="pointer fa fa-arrow-up"
aria-label="Variable editor Table ArrowUp button"
></i>
</td>
<td style="width: 1%">
<i ng-click="_.move(variables,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i>
<i
ng-click="_.move(variables,$index,$index+1)"
ng-hide="$last"
class="pointer fa fa-arrow-down"
aria-label="Variable editor Table ArrowDown button"
></i>
</td>
<td style="width: 1%">
<a ng-click="duplicate(variable)" class="btn btn-inverse btn-small">
<a
ng-click="duplicate(variable)"
class="btn btn-inverse btn-small"
aria-label="Variable editor Table Duplicate button"
>
Duplicate
</a>
</td>
<td style="width: 1%">
<a ng-click="removeVariable(variable)" class="btn btn-danger btn-small">
<a
ng-click="removeVariable(variable)"
class="btn btn-danger btn-small"
aria-label="Variable editor Table Remove button"
>
<i class="fa fa-remove"></i>
</a>
</td>
@ -68,7 +103,7 @@
</div>
</div>
<form ng-if="mode === 'edit' || mode === 'new'" name="ctrl.form">
<form ng-if="mode === 'edit' || mode === 'new'" name="ctrl.form" aria-label="Variable editor Form">
<h5 class="section-heading">General</h5>
<div class="gf-form-group">
<div class="gf-form-inline">
@ -82,6 +117,7 @@
ng-model="current.name"
required
ng-pattern="namePattern"
aria-label="Variable editor Form Name field"
/>
</div>
<div class="gf-form max-width-19">
@ -97,6 +133,7 @@
ng-model="current.type"
ng-options="k as v.name for (k, v) in variableTypes"
ng-change="typeChanged()"
aria-label="Variable editor Form Type select"
></select>
</div>
</div>
@ -111,7 +148,13 @@
<div class="gf-form-inline">
<div class="gf-form max-width-19">
<span class="gf-form-label width-6">Label</span>
<input type="text" class="gf-form-input" ng-model="current.label" placeholder="optional display name" />
<input
type="text"
class="gf-form-input"
ng-model="current.label"
placeholder="optional display name"
aria-label="Variable editor Form Label field"
/>
</div>
<div class="gf-form max-width-19">
<span class="gf-form-label width-6">Hide</span>
@ -120,6 +163,7 @@
class="gf-form-input"
ng-model="current.hide"
ng-options="f.value as f.text for f in hideOptions"
aria-label="Variable editor Form Hide select"
></select>
</div>
</div>
@ -139,6 +183,7 @@
ng-model-onblur
ng-change="runQuery()"
required
aria-label="Variable editor Form Interval Query field"
/>
</div>
@ -149,6 +194,7 @@
label-class="width-9"
checked="current.auto"
on-change="runQuery()"
aria-label="Variable editor Form Interval AutoOption switch"
>
</gf-form-switch>
@ -162,6 +208,7 @@
ng-model="current.auto_count"
ng-options="f for f in [1,2,3,4,5,10,20,30,40,50,100,200,300,400,500]"
ng-change="runQuery()"
aria-label="Variable editor Form Interval AutoCount select"
></select>
</div>
</div>
@ -176,6 +223,7 @@
ng-model="current.auto_min"
ng-change="runQuery()"
placeholder="10s"
aria-label="Variable editor Form Interval AutoMin field"
/>
</div>
</div>
@ -192,6 +240,7 @@
ng-blur="runQuery()"
placeholder="1, 10, 20, myvalue, escaped\,value"
required
aria-label="Variable editor Form Custom Query field"
/>
</div>
</div>
@ -206,6 +255,7 @@
ng-model="current.query"
ng-blur="runQuery()"
placeholder="your metric prefix"
aria-label="Variable editor Form Constant Query field"
/>
</div>
</div>
@ -220,6 +270,7 @@
ng-model="current.query"
ng-blur="runQuery()"
placeholder="default value, if any"
aria-label="Variable editor Form TextBox Query field"
/>
</div>
</div>
@ -237,6 +288,7 @@
ng-options="f.value as f.name for f in datasources"
ng-change="datasourceChanged()"
required
aria-label="Variable editor Form Query DataSource select"
>
<option value="" ng-if="false"></option>
</select>
@ -255,6 +307,7 @@
class="gf-form-input"
ng-model="current.refresh"
ng-options="f.value as f.text for f in refreshOptions"
aria-label="Variable editor Form Query Refresh select"
></select>
</div>
</div>
@ -278,6 +331,7 @@
placeholder="/.*-(.*)-.*/"
ng-model-onblur
ng-change="runQuery()"
aria-label="Variable editor Form Query RegEx field"
/>
</div>
<div class="gf-form max-width-21">
@ -293,6 +347,7 @@
ng-model="current.sort"
ng-options="f.value as f.text for f in sortOptions"
ng-change="runQuery()"
aria-label="Variable editor Form Query Sort select"
></select>
</div>
</div>
@ -309,6 +364,7 @@
ng-model="current.query"
ng-options="f.value as f.text for f in datasourceTypes"
ng-change="runQuery()"
aria-label="Variable editor Form DataSource Query field"
></select>
</div>
</div>
@ -330,6 +386,7 @@
placeholder="/.*-(.*)-.*/"
ng-model-onblur
ng-change="runQuery()"
aria-label="Variable editor Form DataSource RegEx field"
/>
</div>
</div>
@ -345,6 +402,7 @@
ng-options="f.value as f.name for f in datasources"
required
ng-change="validate()"
aria-label="Variable editor Form AdHoc DataSource select"
>
<option value="" ng-if="false"></option>
</select>
@ -362,6 +420,7 @@
tooltip="Enables multiple values to be selected at the same time"
checked="current.multi"
on-change="runQuery()"
aria-label="Variable editor Form Multi switch"
>
</gf-form-switch>
<gf-form-switch
@ -370,12 +429,19 @@
label-class="width-10"
checked="current.includeAll"
on-change="runQuery()"
aria-label="Variable editor Form IncludeAll switch"
>
</gf-form-switch>
</div>
<div class="gf-form" ng-if="current.includeAll">
<span class="gf-form-label width-10">Custom all value</span>
<input type="text" class="gf-form-input max-width-15" ng-model="current.allValue" placeholder="blank = auto" />
<input
type="text"
class="gf-form-input max-width-15"
ng-model="current.allValue"
placeholder="blank = auto"
aria-label="Variable editor Form IncludeAll field"
/>
</div>
</div>
@ -387,6 +453,7 @@
label-class="width-10"
checked="current.useTags"
on-change="runQuery()"
aria-label="Variable editor Form Query UseTags switch"
>
</gf-form-switch>
<div class="gf-form last" ng-if="current.useTags">
@ -397,6 +464,7 @@
ng-model="current.tagsQuery"
placeholder="metric name or tags query"
ng-model-onblur
aria-label="Variable editor Form Query TagsQuery field"
/>
</div>
<div class="gf-form" ng-if="current.useTags">
@ -407,6 +475,7 @@
ng-model="current.tagValuesQuery"
placeholder="apps.$tag.*"
ng-model-onblur
aria-label="Variable editor Form Query TagsValuesQuery field"
/>
</div>
</div>
@ -415,21 +484,43 @@
<h5>Preview of values</h5>
<div class="gf-form-inline">
<div class="gf-form" ng-repeat="option in current.options | limitTo: optionsLimit">
<span class="gf-form-label">{{ option.text }}</span>
<span class="gf-form-label" aria-label="Variable editor Preview of Values option">{{ option.text }}</span>
</div>
<div class="gf-form" ng-if="current.options.length > optionsLimit">
<a class="gf-form-label btn-secondary" ng-click="showMoreOptions()">Show more</a>
<a
class="gf-form-label btn-secondary"
ng-click="showMoreOptions()"
aria-label="Variable editor Preview of Values Show More link"
>
Show more
</a>
</div>
</div>
</div>
<div class="alert alert-info gf-form-group" ng-if="infoText">
<div class="alert alert-info gf-form-group" ng-if="infoText" aria-label="Variable editor Form Alert">
{{ infoText }}
</div>
<div class="gf-form-button-row p-y-0">
<button type="submit" class="btn btn-primary" ng-show="mode === 'edit'" ng-click="update();">Update</button>
<button type="submit" class="btn btn-primary" ng-show="mode === 'new'" ng-click="add();">Add</button>
<button
type="submit"
class="btn btn-primary"
ng-show="mode === 'edit'"
ng-click="update();"
aria-label="Variable editor Update button"
>
Update
</button>
<button
type="submit"
class="btn btn-primary"
ng-show="mode === 'new'"
ng-click="add();"
aria-label="Variable editor Add button"
>
Add
</button>
</div>
</form>
</div>

View File

@ -1,5 +1,9 @@
<div class="variable-link-wrapper">
<a ng-click="vm.show()" class="variable-value-link">
<a
ng-click="vm.show()"
class="variable-value-link"
aria-label="Dashboard template variables Variable Value DropDown value link"
>
{{vm.linkText}}
<span ng-repeat="tag in vm.selectedTags" bs-tooltip='tag.valuesText' data-placement="bottom">
<span class="label-tag"tag-color-from-name="tag.text">
@ -12,16 +16,34 @@
<input type="text" class="gf-form-input" style="display: none" ng-keydown="vm.keyDown($event)" ng-model="vm.search.query" ng-change="vm.debouncedQueryChanged()" ></input>
<div class="variable-value-dropdown" ng-if="vm.dropdownVisible" ng-class="{'multi': vm.variable.multi, 'single': !vm.variable.multi}">
<div
class="variable-value-dropdown"
ng-if="vm.dropdownVisible"
ng-class="{'multi': vm.variable.multi, 'single': !vm.variable.multi}"
aria-label="Dashboard template variables Variable Value DropDown DropDown"
>
<div class="variable-options-wrapper">
<div class="variable-options-column">
<a class="variable-options-column-header" ng-if="vm.variable.multi" ng-class="{'many-selected': vm.selectedValues.length > 1}" bs-tooltip="'Clear selections'" data-placement="top" ng-click="vm.clearSelections()">
<a
class="variable-options-column-header"
ng-if="vm.variable.multi"
ng-class="{'many-selected': vm.selectedValues.length > 1}"
bs-tooltip="'Clear selections'"
data-placement="top"
ng-click="vm.clearSelections()"
aria-label="Dashboard template variables Variable Value DropDown Selected link"
>
<span class="variable-option-icon"></span>
Selected ({{vm.selectedValues.length}})
</a>
<a class="variable-option pointer" ng-repeat="option in vm.search.options" ng-class="{'selected': option.selected, 'highlighted': $index === vm.highlightIndex}" ng-click="vm.selectValue(option, $event)">
<a
class="variable-option pointer"
ng-repeat="option in vm.search.options"
ng-class="{'selected': option.selected, 'highlighted': $index === vm.highlightIndex}"
ng-click="vm.selectValue(option, $event)"
>
<span class="variable-option-icon"></span>
<span>{{option.text}}</span>
<span aria-label="Dashboard template variables Variable Value DropDown option text">{{option.text}}</span>
</a>
</div>
<div class="variable-options-column" ng-if="vm.tags.length">

View File

@ -1,12 +0,0 @@
import { TestPage, ClickablePageObjectType, ClickablePageObject, Selector } from '@grafana/toolkit/src/e2e';
export interface CreateDashboardPage {
addQuery: ClickablePageObjectType;
}
export const createDashboardPage = new TestPage<CreateDashboardPage>({
url: '/dashboard/new',
pageObjects: {
addQuery: new ClickablePageObject(Selector.fromAriaLabel('Add Query CTA button')),
},
});

View File

@ -1,11 +0,0 @@
import { ClickablePageObject, ClickablePageObjectType, Selector, TestPage } from '@grafana/toolkit/src/e2e';
export interface DashboardPage {
settings: ClickablePageObjectType;
}
export const dashboardPage = new TestPage<DashboardPage>({
pageObjects: {
settings: new ClickablePageObject(Selector.fromAriaLabel('Dashboard settings navbar button')),
},
});

View File

@ -1,11 +0,0 @@
import { ClickablePageObject, ClickablePageObjectType, Selector, TestPage } from '@grafana/toolkit/src/e2e';
export interface DashboardSettingsPage {
deleteDashBoard: ClickablePageObjectType;
}
export const dashboardSettingsPage = new TestPage<DashboardSettingsPage>({
pageObjects: {
deleteDashBoard: new ClickablePageObject(Selector.fromAriaLabel('Dashboard settings page delete dashboard button')),
},
});

View File

@ -1,23 +0,0 @@
import {
TestPage,
ClickablePageObjectType,
ClickablePageObject,
Selector,
InputPageObjectType,
InputPageObject,
PageObject,
} from '@grafana/toolkit/src/e2e';
export interface SaveDashboardModal {
name: InputPageObjectType;
save: ClickablePageObjectType;
success: PageObject;
}
export const saveDashboardModal = new TestPage<SaveDashboardModal>({
pageObjects: {
name: new InputPageObject(Selector.fromAriaLabel('Save dashboard title field')),
save: new ClickablePageObject(Selector.fromAriaLabel('Save dashboard button')),
success: new PageObject(Selector.fromSelector('.alert-success')),
},
});

View File

@ -1,12 +0,0 @@
import { TestPage, ClickablePageObject, Selector, ClickablePageObjectType } from '@grafana/toolkit/src/e2e';
export interface AddDataSourcePage {
testDataDB: ClickablePageObjectType;
}
export const addDataSourcePage = new TestPage<AddDataSourcePage>({
url: '/datasources/new',
pageObjects: {
testDataDB: new ClickablePageObject(Selector.fromAriaLabel('TestData DB datasource plugin')),
},
});

View File

@ -1,13 +0,0 @@
import { ClickablePageObject, ClickablePageObjectType, Selector, TestPage } from '@grafana/toolkit/src/e2e';
export interface DataSourcesPage {
testData: ClickablePageObjectType;
}
export const dataSourcesPageFactory = (testDataSourceName: string) =>
new TestPage<DataSourcesPage>({
url: '/datasources',
pageObjects: {
testData: new ClickablePageObject(Selector.fromAriaLabel(`Data source list item for ${testDataSourceName}`)),
},
});

View File

@ -1,28 +0,0 @@
import {
ClickablePageObject,
ClickablePageObjectType,
InputPageObject,
InputPageObjectType,
PageObject,
PageObjectType,
Selector,
TestPage,
} from '@grafana/toolkit/src/e2e';
export interface EditDataSourcePage {
name: InputPageObjectType;
delete: ClickablePageObjectType;
saveAndTest: ClickablePageObjectType;
alert: PageObjectType;
alertMessage: PageObjectType;
}
export const editDataSourcePage = new TestPage<EditDataSourcePage>({
pageObjects: {
name: new InputPageObject(Selector.fromAriaLabel('Datasource settings page name input field')),
delete: new ClickablePageObject(Selector.fromAriaLabel('Delete button')),
saveAndTest: new ClickablePageObject(Selector.fromAriaLabel('Save and Test button')),
alert: new PageObject(Selector.fromAriaLabel('Datasource settings page Alert')),
alertMessage: new PageObject(Selector.fromAriaLabel('Datasource settings page Alert message')),
},
});

View File

@ -1,13 +0,0 @@
import { ClickablePageObject, ClickablePageObjectType, PageObject, Selector, TestPage } from '@grafana/toolkit/src/e2e';
export interface ConfirmModal {
delete: ClickablePageObjectType;
success: PageObject;
}
export const confirmModal = new TestPage<ConfirmModal>({
pageObjects: {
delete: new ClickablePageObject(Selector.fromAriaLabel('Confirm Modal Danger Button')),
success: new PageObject(Selector.fromSelector('.alert-success')),
},
});

View File

@ -1,11 +1,4 @@
import {
TestPage,
SelectPageObjectType,
SelectPageObject,
Selector,
ClickablePageObjectType,
ClickablePageObject,
} from '@grafana/toolkit/src/e2e';
import { ClickablePageObjectType, Selector, SelectPageObjectType, TestPage } from '@grafana/toolkit/src/e2e';
export interface EditPanelPage {
queriesTab: ClickablePageObjectType;
@ -17,12 +10,10 @@ export interface EditPanelPage {
export const editPanelPage = new TestPage<EditPanelPage>({
pageObjects: {
queriesTab: new ClickablePageObject(Selector.fromAriaLabel('Queries tab button')),
saveDashboard: new ClickablePageObject(Selector.fromAriaLabel('Save dashboard navbar button')),
scenarioSelect: new SelectPageObject(Selector.fromAriaLabel('Scenario Select')),
showXAxis: new ClickablePageObject(
Selector.fromSelector('[aria-label="X-Axis section"] [label=Show] .gf-form-switch')
),
visualizationTab: new ClickablePageObject(Selector.fromAriaLabel('Visualization tab button')),
queriesTab: 'Queries tab button',
saveDashboard: 'Save dashboard navbar button',
scenarioSelect: 'Scenario Select',
showXAxis: () => Selector.fromSelector('[aria-label="X-Axis section"] [label=Show] .gf-form-switch'),
visualizationTab: 'Visualization tab button',
},
});

View File

@ -1,4 +1,4 @@
import { TestPage, ClickablePageObjectType, ClickablePageObject, Selector } from '@grafana/toolkit/src/e2e';
import { ClickablePageObjectType, TestPage } from '@grafana/toolkit/src/e2e';
export interface Panel {
panelTitle: ClickablePageObjectType;
@ -7,7 +7,7 @@ export interface Panel {
export const panel = new TestPage<Panel>({
pageObjects: {
panelTitle: new ClickablePageObject(Selector.fromAriaLabel('Panel Title')),
share: new ClickablePageObject(Selector.fromAriaLabel('Share panel menu item')),
panelTitle: 'Panel Title',
share: 'Share panel menu item',
},
});

View File

@ -1,4 +1,4 @@
import { TestPage, ClickablePageObjectType, ClickablePageObject, Selector } from '@grafana/toolkit/src/e2e';
import { ClickablePageObjectType, TestPage } from '@grafana/toolkit/src/e2e';
export interface SharePanelModal {
directLinkRenderedImage: ClickablePageObjectType;
@ -6,6 +6,6 @@ export interface SharePanelModal {
export const sharePanelModal = new TestPage<SharePanelModal>({
pageObjects: {
directLinkRenderedImage: new ClickablePageObject(Selector.fromAriaLabel('Link to rendered image')),
directLinkRenderedImage: 'Link to rendered image',
},
});

View File

@ -0,0 +1,92 @@
import {
ClickablePageObjectType,
InputPageObjectType,
PageObjectType,
Selector,
SelectPageObjectType,
SwitchPageObjectType,
TestPage,
} from '@grafana/toolkit/src/e2e';
export interface VariablePage {
headerLink: ClickablePageObjectType;
modeLabel: PageObjectType;
generalNameInput: InputPageObjectType;
generalTypeSelect: SelectPageObjectType;
generalLabelInput: InputPageObjectType;
generalHideSelect: SelectPageObjectType;
queryOptionsDataSourceSelect: SelectPageObjectType;
queryOptionsRefreshSelect: SelectPageObjectType;
queryOptionsRegExInput: InputPageObjectType;
queryOptionsSortSelect: SelectPageObjectType;
queryOptionsQueryInput: InputPageObjectType;
selectionOptionsMultiSwitch: SwitchPageObjectType;
selectionOptionsIncludeAllSwitch: SwitchPageObjectType;
selectionOptionsCustomAllInput: InputPageObjectType;
valueGroupsTagsEnabledSwitch: SwitchPageObjectType;
valueGroupsTagsTagsQueryInput: InputPageObjectType;
valueGroupsTagsTagsValuesQueryInput: InputPageObjectType;
previewOfValuesOption: PageObjectType;
addButton: ClickablePageObjectType;
updateButton: ClickablePageObjectType;
}
export const variablePage = new TestPage<VariablePage>({
pageObjects: {
headerLink: 'Variable editor Header link',
modeLabel: 'Variable editor Header mode New',
generalNameInput: 'Variable editor Form Name field',
generalTypeSelect: 'Variable editor Form Type select',
generalLabelInput: 'Variable editor Form Label field',
generalHideSelect: 'Variable editor Form Hide select',
queryOptionsDataSourceSelect: 'Variable editor Form Query DataSource select',
queryOptionsRefreshSelect: 'Variable editor Form Query Refresh select',
queryOptionsRegExInput: 'Variable editor Form Query RegEx field',
queryOptionsSortSelect: 'Variable editor Form Query Sort select',
queryOptionsQueryInput: 'Variable editor Form Default Variable Query Editor textarea',
selectionOptionsMultiSwitch: () => Selector.fromSwitchLabel('Variable editor Form Multi switch'),
selectionOptionsIncludeAllSwitch: () => Selector.fromSwitchLabel('Variable editor Form IncludeAll switch'),
selectionOptionsCustomAllInput: 'Variable editor Form IncludeAll field',
valueGroupsTagsEnabledSwitch: () => Selector.fromSwitchLabel('Variable editor Form Query UseTags switch'),
valueGroupsTagsTagsQueryInput: 'Variable editor Form Query TagsQuery field',
valueGroupsTagsTagsValuesQueryInput: 'Variable editor Form Query TagsValuesQuery field',
previewOfValuesOption: 'Variable editor Preview of Values option',
addButton: 'Variable editor Add button',
updateButton: 'Variable editor Update button',
},
});
export interface CreateQueryVariableArguments {
page: TestPage<VariablePage>;
name: string;
label: string;
datasourceName: string;
query: string;
}
export const createQueryVariable = async ({
page,
name,
label,
datasourceName,
query,
}: CreateQueryVariableArguments) => {
console.log('Creating a Query Variable with required');
await page.pageObjects.generalNameInput.enter(name);
await page.pageObjects.generalLabelInput.enter(label);
await page.pageObjects.queryOptionsDataSourceSelect.select(`string:${datasourceName}`);
await page.pageObjects.queryOptionsQueryInput.exists();
await page.pageObjects.queryOptionsQueryInput.containsPlaceholder('metric name or tags query');
await page.pageObjects.queryOptionsQueryInput.enter(query);
await page.pageObjects.queryOptionsQueryInput.blur();
await page.pageObjects.previewOfValuesOption.exists();
await page.pageObjects.selectionOptionsMultiSwitch.toggle();
await page.pageObjects.selectionOptionsMultiSwitch.isSwitchedOn();
await page.pageObjects.selectionOptionsIncludeAllSwitch.toggle();
await page.pageObjects.selectionOptionsIncludeAllSwitch.isSwitchedOn();
await page.pageObjects.selectionOptionsCustomAllInput.exists();
await page.pageObjects.selectionOptionsCustomAllInput.containsText('');
await page.pageObjects.selectionOptionsCustomAllInput.containsPlaceholder('blank = auto');
await page.pageObjects.addButton.click();
console.log('Creating a Query Variable with required, OK!');
};

View File

@ -0,0 +1,49 @@
import { ArrayPageObjectType, ClickablePageObjectType, TestPage } from '@grafana/toolkit/src/e2e';
export interface VariablesPage {
callToActionButton: ClickablePageObjectType;
variableTableNameField: ArrayPageObjectType;
variableTableDefinitionField: ArrayPageObjectType;
variableTableArrowUpButton: ArrayPageObjectType;
variableTableArrowDownButton: ArrayPageObjectType;
variableTableDuplicateButton: ArrayPageObjectType;
variableTableRemoveButton: ArrayPageObjectType;
newVariableButton: ClickablePageObjectType;
goBackButton: ClickablePageObjectType;
}
export const variablesPage = new TestPage<VariablesPage>({
pageObjects: {
callToActionButton: 'Call to action button Add variable',
variableTableNameField: 'Variable editor Table Name field',
variableTableDefinitionField: 'Variable editor Table Definition field',
variableTableArrowUpButton: 'Variable editor Table ArrowUp button',
variableTableArrowDownButton: 'Variable editor Table ArrowDown button',
variableTableDuplicateButton: 'Variable editor Table Duplicate button',
variableTableRemoveButton: 'Variable editor Table Remove button',
newVariableButton: 'Variable editor New variable button',
goBackButton: 'Dashboard settings Go Back button',
},
});
export interface AssertVariableTableArguments {
name: string;
query: string;
}
export const assertVariableTable = async (page: TestPage<VariablesPage>, args: AssertVariableTableArguments[]) => {
console.log('Asserting variable table');
await page.pageObjects.variableTableNameField.waitForSelector();
await page.pageObjects.variableTableNameField.hasLength(args.length);
await page.pageObjects.variableTableDefinitionField.hasLength(args.length);
await page.pageObjects.variableTableArrowUpButton.hasLength(args.length);
await page.pageObjects.variableTableArrowDownButton.hasLength(args.length);
await page.pageObjects.variableTableDuplicateButton.hasLength(args.length);
await page.pageObjects.variableTableRemoveButton.hasLength(args.length);
for (let index = 0; index < args.length; index++) {
const { name, query } = args[index];
await page.pageObjects.variableTableNameField.containsTextAtPos(`$${name}`, index);
await page.pageObjects.variableTableDefinitionField.containsTextAtPos(query, index);
}
console.log('Asserting variable table, Ok');
};

View File

@ -1,50 +1,19 @@
import { Browser, Page, Target } from 'puppeteer-core';
import { compareScreenShots, constants, e2eScenario, takeScreenShot } from '@grafana/toolkit/src/e2e';
import { addDataSourcePage } from 'e2e-test/pages/datasources/addDataSourcePage';
import { editDataSourcePage } from 'e2e-test/pages/datasources/editDataSourcePage';
import { dataSourcesPageFactory } from 'e2e-test/pages/datasources/dataSources';
import { createDashboardPage } from 'e2e-test/pages/dashboards/createDashboardPage';
import { saveDashboardModal } from 'e2e-test/pages/dashboards/saveDashboardModal';
import { dashboardsPageFactory } from 'e2e-test/pages/dashboards/dashboardsPage';
import {
cleanDashboard,
createDashboardPage,
dashboardsPageFactory,
saveDashboardModal,
} from '@grafana/toolkit/src/e2e/pages';
import { panel } from 'e2e-test/pages/panels/panel';
import { editPanelPage } from 'e2e-test/pages/panels/editPanel';
import { sharePanelModal } from 'e2e-test/pages/panels/sharePanelModal';
import { confirmModal } from '../pages/modals/confirmModal';
import { dashboardPage } from '../pages/dashboards/dashboardPage';
import { dashboardSettingsPage } from '../pages/dashboards/dashboardSettingsPage';
const addTestDataSourceAndVerify = async (page: Page) => {
// Add TestData DB
const testDataSourceName = `TestData-${new Date().getTime()}`;
await addDataSourcePage.init(page);
await addDataSourcePage.navigateTo();
await addDataSourcePage.pageObjects.testDataDB.exists();
await addDataSourcePage.pageObjects.testDataDB.click();
await editDataSourcePage.init(page);
await editDataSourcePage.waitForNavigation();
await editDataSourcePage.pageObjects.name.enter(testDataSourceName);
await editDataSourcePage.pageObjects.saveAndTest.click();
await editDataSourcePage.pageObjects.alert.exists();
await editDataSourcePage.pageObjects.alertMessage.containsText('Data source is working');
// Verify that data source is listed
const url = await editDataSourcePage.getUrlWithoutBaseUrl();
const expectedUrl = url.substring(1, url.length - 1);
const selector = `a[href="${expectedUrl}"]`;
const dataSourcesPage = dataSourcesPageFactory(testDataSourceName);
await dataSourcesPage.init(page);
await dataSourcesPage.navigateTo();
await dataSourcesPage.expectSelector({ selector });
return testDataSourceName;
};
const addDashboardAndSetupTestDataGraph = async (page: Page) => {
export const addDashboardAndSetupTestDataGraph = async (page: Page) => {
// Create a new Dashboard
const dashboardTitle = `Dashboard-${new Date().getTime()}`;
const dashboardTitle = `e2e - Dashboard-${new Date().getTime()}`;
await createDashboardPage.init(page);
await createDashboardPage.navigateTo();
await createDashboardPage.pageObjects.addQuery.click();
@ -67,7 +36,11 @@ const addDashboardAndSetupTestDataGraph = async (page: Page) => {
return dashboardTitle;
};
const clickOnSharePanelImageLinkAndCompareImages = async (browser: Browser, page: Page, dashboardTitle: string) => {
export const clickOnSharePanelImageLinkAndCompareImages = async (
browser: Browser,
page: Page,
dashboardTitle: string
) => {
// Share the dashboard
const dashboardsPage = dashboardsPageFactory(dashboardTitle);
await dashboardsPage.init(page);
@ -95,47 +68,14 @@ const clickOnSharePanelImageLinkAndCompareImages = async (browser: Browser, page
}
};
const cleanUpTestDataSource = async (page: Page, testDataSourceName: string) => {
const dataSourcesPage = dataSourcesPageFactory(testDataSourceName);
await dataSourcesPage.init(page);
await dataSourcesPage.navigateTo();
await dataSourcesPage.pageObjects.testData.click();
await editDataSourcePage.init(page);
await editDataSourcePage.pageObjects.delete.exists();
await editDataSourcePage.pageObjects.delete.click();
await confirmModal.init(page);
await confirmModal.pageObjects.delete.click();
await confirmModal.pageObjects.success.exists();
};
const cleanDashboard = async (page: Page, dashboardTitle: string) => {
const dashboardsPage = dashboardsPageFactory(dashboardTitle);
await dashboardsPage.init(page);
await dashboardsPage.navigateTo();
await dashboardsPage.pageObjects.dashboard.exists();
await dashboardsPage.pageObjects.dashboard.click();
await dashboardPage.init(page);
await dashboardPage.pageObjects.settings.click();
await dashboardSettingsPage.init(page);
await dashboardSettingsPage.pageObjects.deleteDashBoard.click();
await confirmModal.init(page);
await confirmModal.pageObjects.delete.click();
await confirmModal.pageObjects.success.exists();
};
e2eScenario(
'Login scenario, create test data source, dashboard, panel, and export scenario',
'should pass',
async (browser: Browser, page: Page) => {
const testDataSourceName = await addTestDataSourceAndVerify(page);
e2eScenario({
describeName: 'Smoke tests',
itName: 'Login scenario, create test data source, dashboard, panel, and export scenario',
skipScenario: false,
createTestDataSource: true,
scenario: async (browser: Browser, page: Page) => {
const dashboardTitle = await addDashboardAndSetupTestDataGraph(page);
await clickOnSharePanelImageLinkAndCompareImages(browser, page, dashboardTitle);
await cleanUpTestDataSource(page, testDataSourceName);
await cleanDashboard(page, dashboardTitle);
}
);
},
});

View File

@ -0,0 +1,103 @@
import { e2eScenario, TestPage } from '@grafana/toolkit/src/e2e';
import { Browser, Page } from 'puppeteer-core';
import {
assertVariableLabelsAndComponents,
DashboardPage,
dashboardSettingsPage,
saveChangesDashboardModal,
} from '@grafana/toolkit/src/e2e/pages';
import { assertVariableTable, variablesPage } from '../../pages/templating/variablesPage';
import { createQueryVariable, variablePage } from '../../pages/templating/variablePage';
e2eScenario({
describeName: 'Template Variables tests',
itName: 'Template Variables QueryVariable CRUD',
createTestDataSource: true,
createTestDashboard: true,
scenario: async (browser: Browser, page: Page, datasourceName?: string, dashboardPage?: TestPage<DashboardPage>) => {
await dashboardPage.pageObjects.settings.click();
await dashboardSettingsPage.init(page);
await dashboardSettingsPage.pageObjects.variablesSection.click();
await variablesPage.init(page);
await variablesPage.pageObjects.callToActionButton.exists();
await variablesPage.pageObjects.callToActionButton.click();
console.log('Asserting defaults for new variable');
await variablePage.init(page);
await variablePage.pageObjects.generalNameInput.exists();
await variablePage.pageObjects.generalNameInput.containsText('');
await variablePage.pageObjects.generalNameInput.containsPlaceholder('name');
await variablePage.pageObjects.generalTypeSelect.exists();
await variablePage.pageObjects.generalTypeSelect.selectedTextIs('Query');
await variablePage.pageObjects.generalLabelInput.exists();
await variablePage.pageObjects.generalLabelInput.containsText('');
await variablePage.pageObjects.generalLabelInput.containsPlaceholder('optional display name');
await variablePage.pageObjects.generalHideSelect.exists();
await variablePage.pageObjects.generalHideSelect.selectedTextIs('');
await variablePage.pageObjects.queryOptionsDataSourceSelect.exists();
await variablePage.pageObjects.queryOptionsDataSourceSelect.selectedTextIs('');
await variablePage.pageObjects.queryOptionsRefreshSelect.exists();
await variablePage.pageObjects.queryOptionsRefreshSelect.selectedTextIs('Never');
await variablePage.pageObjects.queryOptionsRegExInput.exists();
await variablePage.pageObjects.queryOptionsRegExInput.containsText('');
await variablePage.pageObjects.queryOptionsRegExInput.containsPlaceholder('/.*-(.*)-.*/');
await variablePage.pageObjects.queryOptionsSortSelect.exists();
await variablePage.pageObjects.queryOptionsSortSelect.selectedTextIs('Disabled');
await variablePage.pageObjects.selectionOptionsMultiSwitch.exists();
await variablePage.pageObjects.selectionOptionsMultiSwitch.isSwitchedOff();
await variablePage.pageObjects.selectionOptionsIncludeAllSwitch.exists();
await variablePage.pageObjects.selectionOptionsIncludeAllSwitch.isSwitchedOff();
await variablePage.pageObjects.valueGroupsTagsEnabledSwitch.exists();
await variablePage.pageObjects.valueGroupsTagsEnabledSwitch.isSwitchedOff();
console.log('Asserting defaults for new variable, OK!');
await variablesPage.pageObjects.goBackButton.click();
await dashboardPage.pageObjects.settings.click();
await dashboardSettingsPage.init(page);
await dashboardSettingsPage.pageObjects.variablesSection.click();
await variablesPage.pageObjects.callToActionButton.exists();
await variablesPage.pageObjects.callToActionButton.click();
const queryVariables = [
{ name: 'query1', query: '*', label: 'query1-label', options: ['All', 'A', 'B', 'C'] },
{ name: 'query2', query: '$query1.*', label: 'query2-label', options: ['All', 'AA', 'AB', 'AC'] },
{ name: 'query3', query: '$query1.$query2.*', label: 'query2-label', options: ['All', 'AAA', 'AAB', 'AAC'] },
];
for (let queryVariableIndex = 0; queryVariableIndex < queryVariables.length; queryVariableIndex++) {
const { name, label, query } = queryVariables[queryVariableIndex];
const asserts = queryVariables.slice(0, queryVariableIndex + 1);
await createQueryVariable({
page: variablePage,
datasourceName,
name,
label,
query,
});
await assertVariableTable(variablesPage, asserts);
await dashboardSettingsPage.pageObjects.saveDashBoard.click();
await saveChangesDashboardModal.init(page);
await saveChangesDashboardModal.pageObjects.save.click();
await saveChangesDashboardModal.pageObjects.success.exists();
await variablesPage.pageObjects.goBackButton.click();
await assertVariableLabelsAndComponents(dashboardPage, asserts);
await dashboardPage.pageObjects.settings.click();
await dashboardSettingsPage.pageObjects.variablesSection.click();
await variablesPage.pageObjects.newVariableButton.click();
}
},
});