From a7ef4d635692f085a05ce975ee283e7fbab914f1 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Fri, 8 May 2026 09:26:50 +0200 Subject: [PATCH] Plugin E2E: Add Select, MultiSelect, Switch, RadioGroup, UnitPicker, and ColorPicker to components fixture Extends the components fixture introduced in #2583 with six additional Grafana UI components. Each component gets a static getContainer() for version-conditional container resolution and a within(root) method for DOM scoping. Uses CSS/structural fallback selectors for all Grafana versions; data-testid selectors will be added once grafana/grafana#124120 merges and @grafana/e2e-selectors is updated. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/plugin-e2e/src/index.ts | 6 ++ packages/plugin-e2e/src/models/Components.ts | 20 ++++ .../src/models/components/ColorPicker.ts | 9 ++ .../src/models/components/MultiSelect.ts | 9 ++ .../src/models/components/RadioGroup.ts | 12 +++ .../src/models/components/Select.ts | 12 +++ .../src/models/components/Switch.ts | 12 +++ .../src/models/components/UnitPicker.ts | 9 ++ .../components/components.spec.ts | 92 +++++++++++++++++++ 9 files changed, 181 insertions(+) diff --git a/packages/plugin-e2e/src/index.ts b/packages/plugin-e2e/src/index.ts index 2db5341673..22169d09b2 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 845f563ad2..617c4a2548 100644 --- a/packages/plugin-e2e/src/models/components/ColorPicker.ts +++ b/packages/plugin-e2e/src/models/components/ColorPicker.ts @@ -10,6 +10,15 @@ export class ColorPicker extends ComponentBase { super(ctx, element); } + static getContainer(ctx: PluginTestCtx, root?: Locator): Locator { + const base = root ?? ctx.page; + return base.locator('[data-testid*="colorswatch"]'); + } + + within(root: Locator): ColorPicker { + return new ColorPicker(this.ctx, ColorPicker.getContainer(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 7a2d728e9d..cc6ebc35b6 100644 --- a/packages/plugin-e2e/src/models/components/MultiSelect.ts +++ b/packages/plugin-e2e/src/models/components/MultiSelect.ts @@ -9,6 +9,15 @@ export class MultiSelect extends ComponentBase { super(ctx, element); } + static getContainer(ctx: PluginTestCtx, root?: Locator): Locator { + const base = root ?? ctx.page; + return base.locator('[class*="-grafana-select-value-container-multi"]'); + } + + 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 4914bd2ed9..1b2f89aaf3 100644 --- a/packages/plugin-e2e/src/models/components/RadioGroup.ts +++ b/packages/plugin-e2e/src/models/components/RadioGroup.ts @@ -9,6 +9,18 @@ export class RadioGroup extends ComponentBase { super(ctx, element); } + static getContainer(ctx: PluginTestCtx, root?: Locator): Locator { + const base = root ?? ctx.page; + if (gte(ctx.grafanaVersion, '10.0.0')) { + return base.locator('[role="radiogroup"]'); + } + return base.locator('div:has(> div > input[type="radio"][name^="radiogroup-"])'); + } + + 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..83b37282ad 100644 --- a/packages/plugin-e2e/src/models/components/Select.ts +++ b/packages/plugin-e2e/src/models/components/Select.ts @@ -9,6 +9,18 @@ export class Select extends ComponentBase { super(ctx, element); } + static getContainer(ctx: PluginTestCtx, root?: Locator): Locator { + const base = root ?? ctx.page; + // TODO: add data-testid branch for >= 13.1.0 once @grafana/e2e-selectors is updated + return base.locator( + '[class*="-grafana-select-value-container"]:not([class*="-grafana-select-value-container-multi"])' + ); + } + + 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 8bda3a3198..ae216b5719 100644 --- a/packages/plugin-e2e/src/models/components/Switch.ts +++ b/packages/plugin-e2e/src/models/components/Switch.ts @@ -12,6 +12,18 @@ export class Switch extends ComponentBase { this.group = group; } + static getContainer(ctx: PluginTestCtx, root?: Locator): Locator { + const base = root ?? ctx.page; + if (gte(ctx.grafanaVersion, '12.0.0')) { + return base.locator('div:has(> input[type="checkbox"][role="switch"])'); + } + return base.locator('div:has(> input[type="checkbox"] + label)'); + } + + 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..756eefc9a4 100644 --- a/packages/plugin-e2e/src/models/components/UnitPicker.ts +++ b/packages/plugin-e2e/src/models/components/UnitPicker.ts @@ -8,6 +8,15 @@ export class UnitPicker extends ComponentBase { super(ctx, element); } + static getContainer(ctx: PluginTestCtx, root?: Locator): Locator { + const base = root ?? ctx.page; + return base.locator('div:has(> div > [data-testid="input-wrapper"] input[placeholder="Choose"])'); + } + + 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'); +});