From 9c005ae89f495e50fdb92cd8a83604d9bf729557 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Fri, 8 May 2026 09:26:50 +0200 Subject: [PATCH 1/9] 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 07b72dab28..2f83361984 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'); +}); From 2aea99fd40e06819dcbdd9df1f262b1ac780a77b Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Mon, 11 May 2026 16:53:13 +0200 Subject: [PATCH 2/9] Plugin E2E: Fix ColorPicker.getContainer to target parent of swatch selectOption() and toHaveColor() both expect the element to be a parent container that wraps the swatch button, not the swatch itself. The colorswatch data-testid lives on the swatch element, so we traverse up one level to match the expected scope. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/plugin-e2e/src/models/components/ColorPicker.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/plugin-e2e/src/models/components/ColorPicker.ts b/packages/plugin-e2e/src/models/components/ColorPicker.ts index 617c4a2548..4e50f94423 100644 --- a/packages/plugin-e2e/src/models/components/ColorPicker.ts +++ b/packages/plugin-e2e/src/models/components/ColorPicker.ts @@ -12,7 +12,10 @@ export class ColorPicker extends ComponentBase { static getContainer(ctx: PluginTestCtx, root?: Locator): Locator { const base = root ?? ctx.page; - return base.locator('[data-testid*="colorswatch"]'); + // The colorswatch data-testid lives on the swatch element itself, but + // selectOption() and toHaveColor() expect the parent container that + // wraps the swatch button and its sibling color-value span. + return base.locator('[data-testid*="colorswatch"]').locator('xpath=..'); } within(root: Locator): ColorPicker { From 2f21d9f3bb86b14829cd4f54c1f17b92fed4f90f Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Mon, 11 May 2026 17:19:40 +0200 Subject: [PATCH 3/9] Plugin E2E: Fix Select and MultiSelect getContainer to target parent The toHaveSelected matcher uses CSS descendant queries that start from the value container class (e.g. div[class*="-grafana-select-value-container"]). Since Playwright's locator() searches descendants only, the element must be a parent that *contains* the value container, not the container itself. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/plugin-e2e/src/models/components/MultiSelect.ts | 4 +++- packages/plugin-e2e/src/models/components/Select.ts | 8 +++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/plugin-e2e/src/models/components/MultiSelect.ts b/packages/plugin-e2e/src/models/components/MultiSelect.ts index 2f83361984..34ae419077 100644 --- a/packages/plugin-e2e/src/models/components/MultiSelect.ts +++ b/packages/plugin-e2e/src/models/components/MultiSelect.ts @@ -11,7 +11,9 @@ export class MultiSelect extends ComponentBase { static getContainer(ctx: PluginTestCtx, root?: Locator): Locator { const base = root ?? ctx.page; - return base.locator('[class*="-grafana-select-value-container-multi"]'); + // 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=..'); } within(root: Locator): MultiSelect { diff --git a/packages/plugin-e2e/src/models/components/Select.ts b/packages/plugin-e2e/src/models/components/Select.ts index 83b37282ad..6d3e144814 100644 --- a/packages/plugin-e2e/src/models/components/Select.ts +++ b/packages/plugin-e2e/src/models/components/Select.ts @@ -12,9 +12,11 @@ export class Select extends ComponentBase { 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"])' - ); + // 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=..'); } within(root: Locator): Select { From bac195c94fbc77849cf2a84686e86322ac90e986 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Mon, 11 May 2026 17:25:33 +0200 Subject: [PATCH 4/9] Plugin E2E: Add .first() to getContainer to avoid strict-mode failures Broad container selectors can match multiple elements on a page. Adding .first() prevents Playwright strict-mode errors when consumers use the unscoped component instance. When scoped via within(root), the root typically narrows to a single match so .first() is harmless. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/plugin-e2e/src/models/components/ColorPicker.ts | 2 +- packages/plugin-e2e/src/models/components/MultiSelect.ts | 2 +- packages/plugin-e2e/src/models/components/RadioGroup.ts | 4 ++-- packages/plugin-e2e/src/models/components/Select.ts | 3 ++- packages/plugin-e2e/src/models/components/Switch.ts | 4 ++-- packages/plugin-e2e/src/models/components/UnitPicker.ts | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/plugin-e2e/src/models/components/ColorPicker.ts b/packages/plugin-e2e/src/models/components/ColorPicker.ts index 4e50f94423..5cb1540ae5 100644 --- a/packages/plugin-e2e/src/models/components/ColorPicker.ts +++ b/packages/plugin-e2e/src/models/components/ColorPicker.ts @@ -15,7 +15,7 @@ export class ColorPicker extends ComponentBase { // The colorswatch data-testid lives on the swatch element itself, but // selectOption() and toHaveColor() expect the parent container that // wraps the swatch button and its sibling color-value span. - return base.locator('[data-testid*="colorswatch"]').locator('xpath=..'); + return base.locator('[data-testid*="colorswatch"]').locator('xpath=..').first(); } within(root: Locator): ColorPicker { diff --git a/packages/plugin-e2e/src/models/components/MultiSelect.ts b/packages/plugin-e2e/src/models/components/MultiSelect.ts index 34ae419077..2d094f75d9 100644 --- a/packages/plugin-e2e/src/models/components/MultiSelect.ts +++ b/packages/plugin-e2e/src/models/components/MultiSelect.ts @@ -13,7 +13,7 @@ export class MultiSelect extends ComponentBase { const base = root ?? ctx.page; // 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=..'); + return base.locator('[class*="-grafana-select-value-container-multi"]').locator('xpath=..').first(); } within(root: Locator): MultiSelect { diff --git a/packages/plugin-e2e/src/models/components/RadioGroup.ts b/packages/plugin-e2e/src/models/components/RadioGroup.ts index 1b2f89aaf3..8d5855cb94 100644 --- a/packages/plugin-e2e/src/models/components/RadioGroup.ts +++ b/packages/plugin-e2e/src/models/components/RadioGroup.ts @@ -12,9 +12,9 @@ export class RadioGroup extends ComponentBase { 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('[role="radiogroup"]').first(); } - return base.locator('div:has(> div > input[type="radio"][name^="radiogroup-"])'); + return base.locator('div:has(> div > input[type="radio"][name^="radiogroup-"])').first(); } within(root: Locator): RadioGroup { diff --git a/packages/plugin-e2e/src/models/components/Select.ts b/packages/plugin-e2e/src/models/components/Select.ts index 6d3e144814..029bedf842 100644 --- a/packages/plugin-e2e/src/models/components/Select.ts +++ b/packages/plugin-e2e/src/models/components/Select.ts @@ -16,7 +16,8 @@ export class Select extends ComponentBase { // 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=..'); + .locator('xpath=..') + .first(); } within(root: Locator): Select { diff --git a/packages/plugin-e2e/src/models/components/Switch.ts b/packages/plugin-e2e/src/models/components/Switch.ts index ae216b5719..fb20fded47 100644 --- a/packages/plugin-e2e/src/models/components/Switch.ts +++ b/packages/plugin-e2e/src/models/components/Switch.ts @@ -15,9 +15,9 @@ export class Switch extends ComponentBase { 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"][role="switch"])').first(); } - return base.locator('div:has(> input[type="checkbox"] + label)'); + return base.locator('div:has(> input[type="checkbox"] + label)').first(); } within(root: Locator): Switch { diff --git a/packages/plugin-e2e/src/models/components/UnitPicker.ts b/packages/plugin-e2e/src/models/components/UnitPicker.ts index 756eefc9a4..317821346e 100644 --- a/packages/plugin-e2e/src/models/components/UnitPicker.ts +++ b/packages/plugin-e2e/src/models/components/UnitPicker.ts @@ -10,7 +10,7 @@ export class UnitPicker extends ComponentBase { 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"])'); + return base.locator('div:has(> div > [data-testid="input-wrapper"] input[placeholder="Choose"])').first(); } within(root: Locator): UnitPicker { From 46f6de3df08be84eddc7ac1674197998a0d37bc0 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Tue, 12 May 2026 08:48:26 +0200 Subject: [PATCH 5/9] Plugin E2E: Fix ColorPicker and RadioGroup container selectors ColorPicker: Pass root directly as element when within(root) is used. The swatch parent is too deep for toHaveColor() which expects the field-level container containing both the swatch button and color value input. This matches PanelEditOptionsGroup.getColorPicker() behavior. RadioGroup: Broaden the < 10.0 fallback selector to also match radio inputs as direct children (not just nested). The clock panel uses direct children with custom name attributes, not the Grafana core name="radiogroup-N" pattern. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plugin-e2e/src/models/components/ColorPicker.ts | 11 +++++------ .../plugin-e2e/src/models/components/RadioGroup.ts | 4 +++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/plugin-e2e/src/models/components/ColorPicker.ts b/packages/plugin-e2e/src/models/components/ColorPicker.ts index 5cb1540ae5..0470af9c65 100644 --- a/packages/plugin-e2e/src/models/components/ColorPicker.ts +++ b/packages/plugin-e2e/src/models/components/ColorPicker.ts @@ -11,15 +11,14 @@ export class ColorPicker extends ComponentBase { } static getContainer(ctx: PluginTestCtx, root?: Locator): Locator { - const base = root ?? ctx.page; - // The colorswatch data-testid lives on the swatch element itself, but - // selectOption() and toHaveColor() expect the parent container that - // wraps the swatch button and its sibling color-value span. - return base.locator('[data-testid*="colorswatch"]').locator('xpath=..').first(); + if (root) { + return root; + } + return ctx.page.locator('[data-testid*="colorswatch"]').locator('xpath=..').first(); } within(root: Locator): ColorPicker { - return new ColorPicker(this.ctx, ColorPicker.getContainer(this.ctx, root)); + return new ColorPicker(this.ctx, root); } async selectOption(rgbOrHex: string, options?: SelectOptionsType): Promise { diff --git a/packages/plugin-e2e/src/models/components/RadioGroup.ts b/packages/plugin-e2e/src/models/components/RadioGroup.ts index 8d5855cb94..22f6a07789 100644 --- a/packages/plugin-e2e/src/models/components/RadioGroup.ts +++ b/packages/plugin-e2e/src/models/components/RadioGroup.ts @@ -14,7 +14,9 @@ export class RadioGroup extends ComponentBase { if (gte(ctx.grafanaVersion, '10.0.0')) { return base.locator('[role="radiogroup"]').first(); } - return base.locator('div:has(> div > input[type="radio"][name^="radiogroup-"])').first(); + return base + .locator('div:has(> input[type="radio"]), div:has(> div > input[type="radio"])') + .first(); } within(root: Locator): RadioGroup { From 80f2fb701b29e60f7daa769dfee4ed405e689e53 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Tue, 12 May 2026 13:41:53 +0200 Subject: [PATCH 6/9] chore: re-trigger CI From 67007a651f88db3410a699af0ace72fadd1b35fd Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Mon, 18 May 2026 11:11:21 +0200 Subject: [PATCH 7/9] Plugin E2E: Use data-testid container selectors for Grafana >= 13.1.0 Bumps @grafana/e2e-selectors to 13.1.0-25893932881 which includes the new stable container data-testids from grafana/grafana#124120. Each getContainer() now uses the data-testid selector for >= 13.1.0 and falls back to CSS/structural selectors for older versions. Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 8 ++++---- packages/plugin-e2e/package.json | 2 +- packages/plugin-e2e/src/models/components/MultiSelect.ts | 5 +++++ packages/plugin-e2e/src/models/components/RadioGroup.ts | 4 ++++ packages/plugin-e2e/src/models/components/Select.ts | 5 ++++- packages/plugin-e2e/src/models/components/Switch.ts | 4 ++++ packages/plugin-e2e/src/models/components/UnitPicker.ts | 5 +++++ 7 files changed, 27 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index c073cf0ca0..4fb9e136f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8576,9 +8576,9 @@ "license": "0BSD" }, "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", @@ -43371,7 +43371,7 @@ "version": "3.8.0", "license": "Apache-2.0", "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" diff --git a/packages/plugin-e2e/package.json b/packages/plugin-e2e/package.json index aefc888890..f723df6a75 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", "semver": "^7.5.4", "uuid": "^13.0.0", "yaml": "^2.3.4" diff --git a/packages/plugin-e2e/src/models/components/MultiSelect.ts b/packages/plugin-e2e/src/models/components/MultiSelect.ts index 2d094f75d9..b0c5c3158a 100644 --- a/packages/plugin-e2e/src/models/components/MultiSelect.ts +++ b/packages/plugin-e2e/src/models/components/MultiSelect.ts @@ -1,8 +1,10 @@ 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) { @@ -11,6 +13,9 @@ export class MultiSelect extends ComponentBase { 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(); diff --git a/packages/plugin-e2e/src/models/components/RadioGroup.ts b/packages/plugin-e2e/src/models/components/RadioGroup.ts index 22f6a07789..a4397824f5 100644 --- a/packages/plugin-e2e/src/models/components/RadioGroup.ts +++ b/packages/plugin-e2e/src/models/components/RadioGroup.ts @@ -3,6 +3,7 @@ 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) { @@ -11,6 +12,9 @@ export class RadioGroup extends ComponentBase { 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(); } diff --git a/packages/plugin-e2e/src/models/components/Select.ts b/packages/plugin-e2e/src/models/components/Select.ts index 029bedf842..1f704c2e12 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 'semver'; import { ComponentBase } from './ComponentBase'; import { SelectOptionsType } from './types'; import { PluginTestCtx } from '../../types'; @@ -11,7 +12,9 @@ export class Select extends ComponentBase { 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 + 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 diff --git a/packages/plugin-e2e/src/models/components/Switch.ts b/packages/plugin-e2e/src/models/components/Switch.ts index fb20fded47..86c78e1876 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 'semver'; +import { resolveGrafanaSelector } from '../utils'; export class Switch extends ComponentBase { private group: Locator; @@ -14,6 +15,9 @@ export class Switch extends ComponentBase { 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(); } diff --git a/packages/plugin-e2e/src/models/components/UnitPicker.ts b/packages/plugin-e2e/src/models/components/UnitPicker.ts index 317821346e..4db03e8022 100644 --- a/packages/plugin-e2e/src/models/components/UnitPicker.ts +++ b/packages/plugin-e2e/src/models/components/UnitPicker.ts @@ -1,7 +1,9 @@ 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) { @@ -10,6 +12,9 @@ export class UnitPicker extends ComponentBase { 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(); } From 5d83023708e5eef2a55fa080278aacf2eaf81462 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Mon, 25 May 2026 10:26:15 +0200 Subject: [PATCH 8/9] Plugin E2E: Fix Select and MultiSelect getContainer to target parent The data-testid Select/MultiSelect container attribute is placed on the value container element itself. The toHaveSelected matcher uses a CSS descendant query starting from that class, so the element must be a parent. Add xpath=.. traversal for the data-testid path, matching the CSS class fallback approach. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/plugin-e2e/src/models/components/MultiSelect.ts | 2 +- packages/plugin-e2e/src/models/components/Select.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-e2e/src/models/components/MultiSelect.ts b/packages/plugin-e2e/src/models/components/MultiSelect.ts index b0c5c3158a..e0dd90d838 100644 --- a/packages/plugin-e2e/src/models/components/MultiSelect.ts +++ b/packages/plugin-e2e/src/models/components/MultiSelect.ts @@ -14,7 +14,7 @@ export class MultiSelect extends ComponentBase { 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(); + 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. diff --git a/packages/plugin-e2e/src/models/components/Select.ts b/packages/plugin-e2e/src/models/components/Select.ts index 1f704c2e12..d214c23b55 100644 --- a/packages/plugin-e2e/src/models/components/Select.ts +++ b/packages/plugin-e2e/src/models/components/Select.ts @@ -13,7 +13,7 @@ export class Select extends ComponentBase { 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(); + 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. From 02bf43333f6c0f4ca6b839b4e7e2ab12b7542da2 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Wed, 24 Jun 2026 22:40:37 +0200 Subject: [PATCH 9/9] ci: bump plugin-actions/e2e-version to v3.0.1 (#2733) Co-authored-by: Claude Opus 4.8 --- .github/workflows/playwright-nightly.yml | 3 +-- .github/workflows/playwright.yml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/playwright-nightly.yml b/.github/workflows/playwright-nightly.yml index f3efaa89b9..0802882d0b 100644 --- a/.github/workflows/playwright-nightly.yml +++ b/.github/workflows/playwright-nightly.yml @@ -25,12 +25,11 @@ jobs: - name: Resolve Grafana E2E versions id: resolve-versions - uses: grafana/plugin-actions/e2e-version@c246b748568a80db2a20ab5c65ff01b8b4f342c4 # e2e-version/v1.2.0 + uses: grafana/plugin-actions/e2e-version@304ed38a05911eda030608b10ff9ca59616e465c # e2e-version/v3.0.1 with: version-resolver-type: plugin-grafana-dependency grafana-dependency: '>=8.5.0' skip-grafana-dev-image: false - skip-grafana-react-19-preview-image: true # limit to latest stable + grafana-dev (2 entries total) limit: 1 diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 23a15740a7..cd75c020bf 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -23,7 +23,7 @@ jobs: - name: Resolve Grafana E2E versions id: resolve-versions - uses: grafana/plugin-actions/e2e-version@73cd57e848b81e75e3c17697e6701c9fa1404bcc # e2e-version/v2.0.0 + uses: grafana/plugin-actions/e2e-version@304ed38a05911eda030608b10ff9ca59616e465c # e2e-version/v3.0.1 with: version-resolver-type: plugin-grafana-dependency grafana-dependency: '>=8.5.0'