diff --git a/package-lock.json b/package-lock.json index d1a69b3d56..f67bd0c478 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8020,9 +8020,9 @@ } }, "node_modules/@grafana/e2e-selectors": { - "version": "13.1.0-25644485979", - "resolved": "https://registry.npmjs.org/@grafana/e2e-selectors/-/e2e-selectors-13.1.0-25644485979.tgz", - "integrity": "sha512-2H+veZBayawey47iUTb5N69QK5KYHxzspe8uaauvLH7aSsjeyzH6nFubLkOVxz1JUOVtVrwOaH+libUwSaPMXw==", + "version": "13.1.0-25893932881", + "resolved": "https://registry.npmjs.org/@grafana/e2e-selectors/-/e2e-selectors-13.1.0-25893932881.tgz", + "integrity": "sha512-wnJGnNBz1Kobhh4nRG6J4d3mUmhTbUconkhMZv79k3aA/rlFA7I3Ru6K56efwW3XF2m3B4qN3Em+AaIJUGMzoQ==", "license": "Apache-2.0", "dependencies": { "semver": "^7.7.0", @@ -36804,7 +36804,7 @@ "version": "3.9.1", "license": "Apache-2.0", "dependencies": { - "@grafana/e2e-selectors": "13.1.0-25644485979", + "@grafana/e2e-selectors": "13.1.0-25893932881", "yaml": "^2.3.4" }, "devDependencies": { diff --git a/packages/plugin-e2e/package.json b/packages/plugin-e2e/package.json index c7ba906c80..565d68f312 100644 --- a/packages/plugin-e2e/package.json +++ b/packages/plugin-e2e/package.json @@ -49,7 +49,7 @@ "dotenv": "^17.2.4" }, "dependencies": { - "@grafana/e2e-selectors": "13.1.0-25644485979", + "@grafana/e2e-selectors": "13.1.0-25893932881", "yaml": "^2.3.4" } } diff --git a/packages/plugin-e2e/src/index.ts b/packages/plugin-e2e/src/index.ts index 146531db63..8d91622916 100644 --- a/packages/plugin-e2e/src/index.ts +++ b/packages/plugin-e2e/src/index.ts @@ -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'; diff --git a/packages/plugin-e2e/src/models/Components.ts b/packages/plugin-e2e/src/models/Components.ts index 9252b7aa01..c0992f0047 100644 --- a/packages/plugin-e2e/src/models/Components.ts +++ b/packages/plugin-e2e/src/models/Components.ts @@ -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. @@ -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)); } } diff --git a/packages/plugin-e2e/src/models/components/ColorPicker.ts b/packages/plugin-e2e/src/models/components/ColorPicker.ts index df82ed3e25..08087a574c 100644 --- a/packages/plugin-e2e/src/models/components/ColorPicker.ts +++ b/packages/plugin-e2e/src/models/components/ColorPicker.ts @@ -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 { await this.element.getByRole('button').click(options); await this.getCustomTab().click(options); diff --git a/packages/plugin-e2e/src/models/components/MultiSelect.ts b/packages/plugin-e2e/src/models/components/MultiSelect.ts index 07b72dab28..09743cc627 100644 --- a/packages/plugin-e2e/src/models/components/MultiSelect.ts +++ b/packages/plugin-e2e/src/models/components/MultiSelect.ts @@ -1,14 +1,30 @@ import { Locator } from '@playwright/test'; +import { gte } from '../../utils/version'; 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)).locator('xpath=..').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)); + } + async selectOptions(values: string[], options?: SelectOptionsType): Promise { const menu = await openSelect(this, options); diff --git a/packages/plugin-e2e/src/models/components/RadioGroup.ts b/packages/plugin-e2e/src/models/components/RadioGroup.ts index 2b1e3a999d..3b0c0232c5 100644 --- a/packages/plugin-e2e/src/models/components/RadioGroup.ts +++ b/packages/plugin-e2e/src/models/components/RadioGroup.ts @@ -3,12 +3,30 @@ import { ComponentBase } from './ComponentBase'; import { CheckOptionsType } from './types'; import { PluginTestCtx } from '../../types'; import { gte } from '../../utils/version'; +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 { if (gte(this.ctx.grafanaVersion, '10.2.0')) { return this.element.getByLabel(labelOrValue, { exact: true }).check(options); diff --git a/packages/plugin-e2e/src/models/components/Select.ts b/packages/plugin-e2e/src/models/components/Select.ts index aca45ca620..5af927eca8 100644 --- a/packages/plugin-e2e/src/models/components/Select.ts +++ b/packages/plugin-e2e/src/models/components/Select.ts @@ -1,4 +1,5 @@ import { Locator } from '@playwright/test'; +import { gte } from '../../utils/version'; import { ComponentBase } from './ComponentBase'; import { SelectOptionsType } from './types'; import { PluginTestCtx } from '../../types'; @@ -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)).locator('xpath=..').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 { const menu = await openSelect(this, options); // type into whichever input gained focus when the select opened - handles virtualized diff --git a/packages/plugin-e2e/src/models/components/Switch.ts b/packages/plugin-e2e/src/models/components/Switch.ts index bae86d8548..0ef3e3b61b 100644 --- a/packages/plugin-e2e/src/models/components/Switch.ts +++ b/packages/plugin-e2e/src/models/components/Switch.ts @@ -3,6 +3,7 @@ import { ComponentBase } from './ComponentBase'; import { CheckOptionsType } from './types'; import { PluginTestCtx } from '../../types'; import { gte, lt } from '../../utils/version'; +import { resolveGrafanaSelector } from '../utils'; export class Switch extends ComponentBase { private group: Locator; @@ -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'); diff --git a/packages/plugin-e2e/src/models/components/UnitPicker.ts b/packages/plugin-e2e/src/models/components/UnitPicker.ts index 0d565c65f2..a2e3c69d93 100644 --- a/packages/plugin-e2e/src/models/components/UnitPicker.ts +++ b/packages/plugin-e2e/src/models/components/UnitPicker.ts @@ -1,13 +1,27 @@ import { Locator } from '@playwright/test'; +import { gte } from '../../utils/version'; 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 { await this.element.getByRole('textbox').click(); const option = await this.getOption(value, options); diff --git a/packages/plugin-e2e/tests/as-admin-user/components/components.spec.ts b/packages/plugin-e2e/tests/as-admin-user/components/components.spec.ts index af78d305d4..fd9ba20c2c 100644 --- a/packages/plugin-e2e/tests/as-admin-user/components/components.spec.ts +++ b/packages/plugin-e2e/tests/as-admin-user/components/components.spec.ts @@ -1,5 +1,9 @@ import { expect, test } from '../../../src'; +/** + * DataSourcePicker + */ + test('components.dataSourcePicker should set the data source', async ({ panelEditPage, components, @@ -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, @@ -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'); +});