Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
759 changes: 35 additions & 724 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/plugin-e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"dotenv": "^17.2.4"
},
"dependencies": {
"@grafana/e2e-selectors": "13.1.0-25644485979",
"@grafana/e2e-selectors": "13.1.0-25893932881",
"semver": "^7.5.4",
"uuid": "^13.0.0",
"yaml": "^2.3.4"
Expand Down
6 changes: 6 additions & 0 deletions packages/plugin-e2e/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,16 @@ import { DashboardPage } from './models/pages/DashboardPage';

// models
export { Components } from './models/Components';
export { ColorPicker } from './models/components/ColorPicker';
export { DataSourcePicker } from './models/components/DataSourcePicker';
export { MultiSelect } from './models/components/MultiSelect';
export { RadioGroup } from './models/components/RadioGroup';
export { ScopedComponent } from './models/components/ScopedComponent';
export { Select } from './models/components/Select';
export { Switch } from './models/components/Switch';
export { Panel } from './models/components/Panel';
export { TimeRange } from './models/components/TimeRange';
export { UnitPicker } from './models/components/UnitPicker';
export { AnnotationEditPage } from './models/pages/AnnotationEditPage';
export { AnnotationPage } from './models/pages/AnnotationPage';
export { DashboardPage } from './models/pages/DashboardPage';
Expand Down
20 changes: 20 additions & 0 deletions packages/plugin-e2e/src/models/Components.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { PluginTestCtx } from '../types';
import { ColorPicker } from './components/ColorPicker';
import { DataSourcePicker } from './components/DataSourcePicker';
import { MultiSelect } from './components/MultiSelect';
import { RadioGroup } from './components/RadioGroup';
import { Select } from './components/Select';
import { Switch } from './components/Switch';
import { TimeRange } from './components/TimeRange';
import { UnitPicker } from './components/UnitPicker';

/**
* Factory for components that are not attached to a specific page.
Expand All @@ -15,14 +21,28 @@ import { TimeRange } from './components/TimeRange';
* ```typescript
* await components.dataSourcePicker.set('prom');
* await components.dataSourcePicker.within(panel).set('prom');
* await components.select.within(fieldLabel).selectOption('Europe/Stockholm');
* await components.switch.within(fieldLabel).check();
* ```
*/
export class Components {
readonly dataSourcePicker: DataSourcePicker;
readonly timeRangePicker: TimeRange;
readonly select: Select;
readonly multiSelect: MultiSelect;
readonly switch: Switch;
readonly radioGroup: RadioGroup;
readonly unitPicker: UnitPicker;
readonly colorPicker: ColorPicker;

constructor(ctx: PluginTestCtx) {
this.dataSourcePicker = new DataSourcePicker(ctx);
this.timeRangePicker = new TimeRange(ctx);
this.select = new Select(ctx, Select.getContainer(ctx));
this.multiSelect = new MultiSelect(ctx, MultiSelect.getContainer(ctx));
this.switch = new Switch(ctx, Switch.getContainer(ctx));
this.radioGroup = new RadioGroup(ctx, RadioGroup.getContainer(ctx));
this.unitPicker = new UnitPicker(ctx, UnitPicker.getContainer(ctx));
this.colorPicker = new ColorPicker(ctx, ColorPicker.getContainer(ctx));
Comment thread
mckn marked this conversation as resolved.
}
}
11 changes: 11 additions & 0 deletions packages/plugin-e2e/src/models/components/ColorPicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ export class ColorPicker extends ComponentBase {
super(ctx, element);
}

static getContainer(ctx: PluginTestCtx, root?: Locator): Locator {
if (root) {
return root;
}
return ctx.page.locator('[data-testid*="colorswatch"]').locator('xpath=..').first();
}

within(root: Locator): ColorPicker {
return new ColorPicker(this.ctx, root);
}

async selectOption(rgbOrHex: string, options?: SelectOptionsType): Promise<void> {
await this.element.getByRole('button').click(options);
await this.getCustomTab().click(options);
Comment thread
mckn marked this conversation as resolved.
Expand Down
16 changes: 16 additions & 0 deletions packages/plugin-e2e/src/models/components/MultiSelect.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
import { Locator } from '@playwright/test';
import { gte } from 'semver';
import { openSelect, selectByValueOrLabel } from './Select';
import { ComponentBase } from './ComponentBase';
import { SelectOptionsType } from './types';
import { PluginTestCtx } from '../../types';
import { resolveGrafanaSelector } from '../utils';

export class MultiSelect extends ComponentBase {
constructor(ctx: PluginTestCtx, element: Locator) {
super(ctx, element);
}

static getContainer(ctx: PluginTestCtx, root?: Locator): Locator {
const base = root ?? ctx.page;
if (gte(ctx.grafanaVersion, '13.1.0')) {
return base.locator(resolveGrafanaSelector(ctx.selectors.components.MultiSelect.container)).first();
}
// The CSS class targets the value container itself, but toHaveSelected uses a
// descendant query starting from that class, so the element must be a parent.
return base.locator('[class*="-grafana-select-value-container-multi"]').locator('xpath=..').first();
}

within(root: Locator): MultiSelect {
return new MultiSelect(this.ctx, MultiSelect.getContainer(this.ctx, root));
Comment thread
mckn marked this conversation as resolved.
}
Comment thread
mckn marked this conversation as resolved.

async selectOptions(values: string[], options?: SelectOptionsType): Promise<string[]> {
const menu = await openSelect(this, options);

Expand Down
18 changes: 18 additions & 0 deletions packages/plugin-e2e/src/models/components/RadioGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,30 @@ import { ComponentBase } from './ComponentBase';
import { CheckOptionsType } from './types';
import { PluginTestCtx } from '../../types';
import { gte } from 'semver';
import { resolveGrafanaSelector } from '../utils';

export class RadioGroup extends ComponentBase {
constructor(ctx: PluginTestCtx, element: Locator) {
super(ctx, element);
}

static getContainer(ctx: PluginTestCtx, root?: Locator): Locator {
const base = root ?? ctx.page;
if (gte(ctx.grafanaVersion, '13.1.0')) {
return base.locator(resolveGrafanaSelector(ctx.selectors.components.RadioGroup.container)).first();
}
if (gte(ctx.grafanaVersion, '10.0.0')) {
return base.locator('[role="radiogroup"]').first();
}
return base
.locator('div:has(> input[type="radio"]), div:has(> div > input[type="radio"])')
.first();
}

within(root: Locator): RadioGroup {
return new RadioGroup(this.ctx, RadioGroup.getContainer(this.ctx, root));
}

async check(labelOrValue: string, options?: CheckOptionsType): Promise<void> {
if (gte(this.ctx.grafanaVersion, '10.2.0')) {
return this.element.getByLabel(labelOrValue, { exact: true }).check(options);
Expand Down
18 changes: 18 additions & 0 deletions packages/plugin-e2e/src/models/components/Select.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Locator } from '@playwright/test';
import { gte } from 'semver';
import { ComponentBase } from './ComponentBase';
import { SelectOptionsType } from './types';
import { PluginTestCtx } from '../../types';
Expand All @@ -9,6 +10,23 @@ export class Select extends ComponentBase {
super(ctx, element);
}

static getContainer(ctx: PluginTestCtx, root?: Locator): Locator {
const base = root ?? ctx.page;
if (gte(ctx.grafanaVersion, '13.1.0')) {
return base.locator(resolveGrafanaSelector(ctx.selectors.components.Select.container)).first();
}
// The CSS class targets the value container itself, but toHaveSelected uses a
// descendant query starting from that class, so the element must be a parent.
return base
.locator('[class*="-grafana-select-value-container"]:not([class*="-grafana-select-value-container-multi"])')
.locator('xpath=..')
.first();
}

within(root: Locator): Select {
return new Select(this.ctx, Select.getContainer(this.ctx, root));
}

async selectOption(values: string, options?: SelectOptionsType): Promise<string> {
const menu = await openSelect(this, options);
// type into whichever input gained focus when the select opened - handles virtualized
Expand Down
16 changes: 16 additions & 0 deletions packages/plugin-e2e/src/models/components/Switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ComponentBase } from './ComponentBase';
import { CheckOptionsType } from './types';
import { PluginTestCtx } from '../../types';
import { gte, lt } from 'semver';
import { resolveGrafanaSelector } from '../utils';

export class Switch extends ComponentBase {
private group: Locator;
Expand All @@ -12,6 +13,21 @@ export class Switch extends ComponentBase {
this.group = group;
}

static getContainer(ctx: PluginTestCtx, root?: Locator): Locator {
const base = root ?? ctx.page;
if (gte(ctx.grafanaVersion, '13.1.0')) {
return base.locator(resolveGrafanaSelector(ctx.selectors.components.Switch.container)).first();
}
if (gte(ctx.grafanaVersion, '12.0.0')) {
return base.locator('div:has(> input[type="checkbox"][role="switch"])').first();
}
return base.locator('div:has(> input[type="checkbox"] + label)').first();
}

within(root: Locator): Switch {
return new Switch(this.ctx, Switch.getContainer(this.ctx, root));
}

private static getElement(ctx: PluginTestCtx, group: Locator): Locator {
if (gte(ctx.grafanaVersion, '11.5.0')) {
return group.getByRole('switch');
Expand Down
14 changes: 14 additions & 0 deletions packages/plugin-e2e/src/models/components/UnitPicker.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
import { Locator } from '@playwright/test';
import { gte } from 'semver';
import { PluginTestCtx } from '../../types';
import { SelectOptionsType } from './types';
import { ComponentBase } from './ComponentBase';
import { resolveGrafanaSelector } from '../utils';

export class UnitPicker extends ComponentBase {
constructor(ctx: PluginTestCtx, element: Locator) {
super(ctx, element);
}

static getContainer(ctx: PluginTestCtx, root?: Locator): Locator {
const base = root ?? ctx.page;
if (gte(ctx.grafanaVersion, '13.1.0')) {
return base.locator(resolveGrafanaSelector(ctx.selectors.components.UnitPicker.container)).first();
}
return base.locator('div:has(> div > [data-testid="input-wrapper"] input[placeholder="Choose"])').first();
}

within(root: Locator): UnitPicker {
return new UnitPicker(this.ctx, UnitPicker.getContainer(this.ctx, root));
}

async selectOption(value: string, options?: SelectOptionsType): Promise<void> {
await this.element.getByRole('textbox').click();
const option = await this.getOption(value, options);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { expect, test } from '../../../src';

/**
* DataSourcePicker
*/

test('components.dataSourcePicker should set the data source', async ({
panelEditPage,
components,
Expand All @@ -22,6 +26,10 @@ test('components.dataSourcePicker.within should set the data source when scoped
await expect(panelEditPage.getQueryEditorRow('A').getByRole('textbox', { name: 'Query Text' })).toBeVisible();
});

/**
* TimeRangePicker
*/

test('components.timeRangePicker should set the time range', async ({
panelEditPage,
components,
Expand All @@ -45,3 +53,87 @@ test('components.timeRangePicker.within should set the time range when scoped to
const openButton = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton).first();
await expect(openButton).toContainText('2020-01-01 00:00:00');
});

/**
* Select
*/

test('components.select.within should select a value in a single-value select', async ({
gotoPanelEditPage,
components,
selectors,
}) => {
const panelEdit = await gotoPanelEditPage({ dashboard: { uid: 'mxb-Jv4Vk' }, id: '5' });
const root = panelEdit.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel('Timezone Timezone')
);
await components.select.within(root).selectOption('Europe/Stockholm');
await expect(components.select.within(root)).toHaveSelected('Europe/Stockholm');
});

/**
* Switch
*/

test('components.switch.within should check a switch', async ({
gotoPanelEditPage,
components,
selectors,
}) => {
const panelEdit = await gotoPanelEditPage({ dashboard: { uid: 'mxb-Jv4Vk' }, id: '5' });
const root = panelEdit.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel('Clock Font monospace')
);
await components.switch.within(root).check();
await expect(components.switch.within(root)).toBeChecked();
});

/**
* RadioGroup
*/

test('components.radioGroup.within should check a radio option', async ({
gotoPanelEditPage,
components,
selectors,
}) => {
const panelEdit = await gotoPanelEditPage({ dashboard: { uid: 'mxb-Jv4Vk' }, id: '5' });
const root = panelEdit.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel('Clock Mode')
);
await components.radioGroup.within(root).check('Countdown');
await expect(components.radioGroup.within(root)).toHaveChecked('Countdown');
});

/**
* ColorPicker
*/

test('components.colorPicker.within should select a color', async ({
gotoPanelEditPage,
components,
selectors,
}) => {
const panelEdit = await gotoPanelEditPage({ dashboard: { uid: 'mxb-Jv4Vk' }, id: '3' });
const root = panelEdit.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel('Clock Background Color')
);
await components.colorPicker.within(root).selectOption('#73bf69');
await expect(components.colorPicker.within(root)).toHaveColor('#73bf69');
});

/**
* UnitPicker
*/

test('components.unitPicker.within should select a unit', async ({
gotoPanelEditPage,
components,
selectors,
}) => {
const panelEdit = await gotoPanelEditPage({ dashboard: { uid: 'be6sir7o1iccgb' }, id: '1' });
const root = panelEdit.getByGrafanaSelector(
selectors.components.PanelEditor.OptionsPane.fieldLabel('Standard options Unit')
);
await components.unitPicker.within(root).selectOption('Misc > Pixels');
});
Loading