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
39 changed files with 910 additions and 317 deletions
+16 -4
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> => {
+104 -17
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);
};
}
@@ -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();
};
@@ -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');
};
@@ -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',
},
});
@@ -0,0 +1,14 @@
import { ClickablePageObjectType } from '../../pageObjects';
import { TestPage } from '../../pageInfo';
export interface DashboardsPage {
dashboard: ClickablePageObjectType;
}
export const dashboardsPageFactory = (dashboardTitle: string) =>
new TestPage<DashboardsPage>({
url: '/dashboards',
pageObjects: {
dashboard: dashboardTitle,
},
});
@@ -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'),
},
});
@@ -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'),
},
});
@@ -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',
},
});
@@ -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();
};
@@ -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',
},
});
@@ -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';
@@ -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',
},
});
@@ -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'),
},
});
+47 -8
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);
});
});
};
@@ -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);
},
});