grafana-datasource/.config/AGENTS/e2e-testing.md
This plugin uses @grafana/plugin-e2e and Playwright for end-to-end testing.
test and expect from @grafana/plugin-e2e, or from your project's fixtures entrypoint (for example ./fixtures) if it re-exports them. Do not import them from @playwright/test directly.@grafana/plugin-e2e fixtures and page models instead of raw Playwright navigation. They handle Grafana version differences automatically.tests/ as *.spec.ts.@grafana/plugin-e2e first. It evolves alongside Grafana core to handle selector and API changes.selectors fixture provided by @grafana/plugin-e2e - never import from @grafana/e2e-selectors directly. The fixture resolves the correct selectors for the Grafana version under test.getByGrafanaSelector method (exposed by all plugin-e2e page models) to resolve selectors to Playwright locators. It handles the aria-label vs data-testid difference across Grafana versions automatically.
panelEditPage.getByGrafanaSelector(selectors.components.CodeEditor.container).click();
Scope locators to the narrowest context possible.
// bad - matches any "URL" text on the page
page.getByText('URL').click();
// good - scoped to the plugin's wrapper
page.getByTestId('plugin-url-wrapper').getByText('URL').click();
The InlineField and Field components can be used interchangeably in the examples below.
Input - use getByRole('textbox', { name: '<label>' }) where the name matches the wrapping InlineField label.
// component
<InlineField label="Auth key">
<Input value={value} onChange={handleOnChange} id="config-auth-key" />
</InlineField>
// test
await page.getByRole('textbox', { name: 'Auth key' }).fill('..');
Select - use getByRole('combobox', { name: '<label>' }) to open the dropdown. Assert options using selectors.components.Select.option via getByGrafanaSelector. Note: the Select component requires inputId (not id) for label association.
// component
<InlineField label="Auth type">
<Select inputId="config-auth-type" value={value} options={options} onChange={handleOnChange} />
</InlineField>
// test
await page.getByRole('combobox', { name: 'Auth type' }).click();
const option = selectors.components.Select.option;
await expect(configPage.getByGrafanaSelector(option)).toHaveText(['val1', 'val2']);
Checkbox - use getByRole('checkbox', { name: '<label>' }). The underlying input is not directly clickable so you must pass { force: true }.
// component
<InlineField label="TLS Enabled">
<Checkbox id="config-tls-enabled" value={value} onChange={handleOnChange} />
</InlineField>
// test
await page.getByRole('checkbox', { name: 'TLS Enabled' }).uncheck({ force: true });
await expect(page.getByRole('checkbox', { name: 'TLS Enabled' })).not.toBeChecked();
InlineSwitch - use getByRole('switch', { name: /<label>/i }). Like Checkbox, requires { force: true }. Prefer getByRole over getByLabel — newer Grafana versions add aria-label to the <label> element itself, causing getByLabel to match multiple elements in strict mode.
// component
<InlineField label="TLS Enabled">
<InlineSwitch label="TLS Enabled" value={value} onChange={handleOnChange} />
</InlineField>
// test
await page.getByRole('switch', { name: /TLS Enabled/i }).uncheck({ force: true });
await expect(page.getByRole('switch', { name: /TLS Enabled/i })).not.toBeChecked();
@grafana/plugin-e2e exposes page models and fixtures that encapsulate common UI operations and handle Grafana version differences.
To discover all available fixtures, options, models and matchers, read the exports in node_modules/@grafana/plugin-e2e/dist/index.d.ts (or refer to the @grafana/plugin-e2e package README/docs).
Fixtures follow a naming convention that indicates how the resource is obtained:
panelEditPage) - creates a new, empty resource. Use when testing from a blank state.goto prefix (e.g. gotoPanelEditPage) - navigates to an existing resource. Use with provisioned dashboards and datasources.readProvisioned prefix (e.g. readProvisionedDataSource) - reads a provisioning file from disk. Use to avoid hardcoding UIDs and names.Default options like featureToggles, user, userPreferences and provisioningRootDir can be overridden per project in playwright.config.ts via use. Per-test overrides work the same way using test.use().
@grafana/plugin-e2e extends Playwright's expect with Grafana-specific assertions:
toBeOK() - asserts a response status is 200-299. Use with saveAndTest(), refreshPanel(), runQuery() and evaluate().toHaveAlert(severity, options?) - asserts an alert box is visible. Severity is 'success', 'warning', 'error' or 'info'. Supports hasText filtering.toDisplayPreviews(values) - asserts variable query preview matches expected values. Use with variableEditPage.await expect(configPage.saveAndTest()).toBeOK();
await expect(configPage).toHaveAlert('error', { hasText: 'API key is missing' });
await expect(variableEditPage).toDisplayPreviews(['value1', 'value2']);
Tests often need pre-configured datasources, dashboards or alert rules. Use Grafana provisioning to set these up - place YAML/JSON files in the provisioning/ folder and they will be loaded when the Grafana test server starts.
readProvisionedDataSource, readProvisionedDashboard and readProvisionedAlertRule fixtures to read values from provisioning files.provisioning/ is not in .gitignore.Tests should be run against multiple Grafana versions. Check grafanaDependency in src/plugin.json for the minimum supported version.
Min supported version:
Use the version from grafanaDependency in plugin.json.
# terminal 1 - replace <min-supported-version> with the grafanaDependency version
GRAFANA_VERSION=<min-supported-version> npm run server
# terminal 2
npm run e2e
Latest dev image (forwards compatibility):
# terminal 1 - use the latest dev image tag from https://hub.docker.com/r/grafana/grafana-dev/tags
GRAFANA_IMAGE=grafana-dev GRAFANA_VERSION=<latest-dev-tag> npm run server
# terminal 2
npm run e2e