/panel-testing-strategy
Write unit and E2E tests for Grafana visualization panels and viz utilities to the conventions this repo expects. Use when adding, backfilling, or reviewing tests for panels (barchart, timeseries, table, xychart, heatmap, canvas, etc.), grafana-ui viz components (Table, uPlot,
Claude Code
$ npx -y skills add grafana/grafana --skill panel-testing-strategy --agent claude-code
Codex
$ npx skills add grafana/grafana --skill panel-testing-strategy -a codex -y
Cursor
$ npx skills add grafana/grafana --skill panel-testing-strategy -a cursor -y
Gemini CLI
$ npx skills add grafana/grafana --skill panel-testing-strategy -a gemini-cli -y
GitHub Copilot
$ npx skills add grafana/grafana --skill panel-testing-strategy -a github-copilot -y
OpenCode
$ npx skills add grafana/grafana --skill panel-testing-strategy -a opencode -y
Windsurf
$ npx skills add grafana/grafana --skill panel-testing-strategy -a windsurf -y
SKILL.md
panel-testing-strategy.SKILL.mdname: panel-testing-strategy
description: Write unit and E2E tests for Grafana visualization panels and viz utilities to the conventions this repo expects. Use when adding, backfilling, or reviewing tests for panels (barchart, timeseries, table, xychart, heatmap, canvas, etc.), grafana-ui viz components (Table, uPlot, VizLegend, VizTooltip), or grafana-data viz utils; when a panel test only asserts "it rendered" or "it's defined"; when reviewing AI-generated panel tests for slop; or when a canvas/rendering test is flaky.
Panel testing strategy
This skill builds on **`frontend-testing-strategy`** — read that first for the general principles every Grafana frontend test is held to (the inverted testing diamond, asserting real behavior instead of existence, avoiding AI slop, verifying a test reaches its target branch, generic anti-flake rules, and the SDLC-phase gating). This skill covers what's specific to visualization code on top of that: data-frame/panel-prop builders, the canvas draw-call snapshot harness, panel accessibility and interaction-snapshot E2E, and canvas/uPlot-specific anti-flake rules. The visualization codeowner paths are opted into the gating `check-frontend-test-coverage.yml` check, so coverage that drops fails CI.
Step 1 — Set up data with the repo's builders
Build data frames with the `@grafana/data` builders — **pick one and don't mix** `toDataFrame` and `createDataFrame` in the same file:
import { createDataFrame, toDataFrame, arrayToDataFrame, FieldType, LoadingState } from '@grafana/data';Use a **single canonical builder per file** with a `Partial<>` overrides object, rather than bespoke frames per test:
function makeFrame(overrides: Partial<Options> = {}) {
/* … */
}To render a panel component, use the shared panel-props builder instead of hand-rolling props:
import { getPanelProps } from '../test-utils'; // public/app/plugins/panel/test-utils.ts
render(<BarChartPanel {...getPanelProps(defaultOptions, { fieldConfig })} />);> **Gotcha — field config.** A panel unit test must call `applyFieldOverrides` itself with a > `createFieldConfigRegistry`; the panel framework normally does this, so without it your > custom `fieldConfig.custom` never reaches the render and every case looks identical.
> **Gotcha — type inference.** If you're testing `guessFieldTypes` (or any inference), feed > **untyped** raw fields (`as unknown as DataFrameDTO`). `createDataFrame` pre-sets `type`, so > the function under test becomes a no-op and the test gives false confidence.
Step 2 — HTML5 canvas / rendering panels: use the draw-call snapshot harness
Panels that draw to canvas (timeseries, heatmap, xychart, timeline, piechart, sparkline) are tested by **capturing the ctx draw-call stream**, not by pixel-diffing. Follow the established harness:
// In the harness (public/app/plugins/panel/timeseries/TimeSeriesPanel.canvasTestUtils.tsx):
import {
applyDefaultUPlotAxisMeasureTextMock,
installCanvasPath2DShim,
removeCanvasTransforms,
} from '@grafana/test-utils/canvas';
// In each *.canvas.test.tsx, mock grafana-ui's text measurement so layout is deterministic:
jest.mock('@grafana/ui/src/utils/measureText', () =>
require('@grafana/test-utils/canvas').createGrafanaUiMeasureTextJestMock(() =>
require('./TimeSeriesPanel.canvasTestUtils').getUPlotInstance()
)
);- Split suites by concern: `*.lines.canvas.test.tsx`, `*.fills.…`, `*.annotations.…`,
`*.axisPlacement.…`, `*.axisRange.…` — each a focused `it.each` of cases.
- Assert with the custom matcher: `expect(events).toMatchCanvasSnapshot(context, { width, height })`.
- **Keep it deterministic** (this is where flake comes from): fixed `width`/`height`, UTC
timestamps (`Date.UTC(...)`, `timeZone: 'utc'`), and wait for the renderer to be ready before asserting — `await waitFor(() => expect(uPlotInstance?.status).toBe(1))` (a `waitFor` callback must **throw** to retry, so it needs `expect`, not a bare boolean).
Step 3 — E2E for interaction, accessibility, and interaction snapshots
The DataViz strategy is **unit-first**. Reserve Playwright for cross-component interaction and per-panel smoke coverage. When you do write E2E:
- Add the selector to the **versioned `@grafana/e2e-selectors` package first**, wire
`data-testid` into the JSX, then query it (use the `add-e2e-selectors` skill).
- Query by selector, never brittle CSS — in E2E `dashboardPage.getByGrafanaSelector(...)`,
in unit `screen.getByTestId(selectors.components...)`.
import { test, expect } from '@grafana/plugin-e2e';
test.describe('Panels test: BarChart render', { tag: ['@panels', '@barchart'] }, () => {
test('renders without error', async ({ gotoDashboardPage, selectors }) => {
const page = await gotoDashboardPage({ uid: DASHBOARD_UID }); // provisioned devenv dashboard
await expect(page.getByGrafanaSelector(selectors.components.Panels.Panel.headerCornerInfo('error'))).toBeHidden();
});
});Accessibility — every panel gets an a11y check
**Every panel must have an E2E accessibility test.** Use the `scanForA11yViolations` fixture and the `toHaveNoA11yViolations()` matcher, in a `describe`/test tagged `@a11y`. Load the panel, wait for it to actually render (assert the panel title and the chart element are visible — an empty panel trivially passes), then scan:
test.describe('a11y', { tag: ['@a11y'] }, () => {
test('run a11y report', async ({ gotoDashboardPage, scanForA11yViolations, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ viewPanel: 'panel-4' }),
});
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('…'))).toBeVisible();
await expect(page.locator('.uplot')).toBeVisible(); // panel has drawn
const report = await scanForA11yViolations({
options: { runOnly: ['wcag2a', 'wcag2aa', 'wcag21a', 'Read more
name: panel-testing-strategy description: Write unit and E2E tests for Grafana visualization panels and viz utilities to the conventions this repo expects. Use when adding, backfilling, or reviewing tests for panels (barchart, timeseries, table, xychart, heatmap, canvas, etc.), grafana-ui viz components (Table, uPlot, VizLegend, VizTooltip), or grafana-data viz utils; when a panel test only asserts "it rendered" or "it's defined"; when reviewing AI-generated panel tests for slop; or when a canvas/rendering test is flaky.
Panel testing strategy
This skill builds on **`frontend-testing-strategy`** — read that first for the general principles every Grafana frontend test is held to (the inverted testing diamond, asserting real behavior instead of existence, avoiding AI slop, verifying a test reaches its target branch, generic anti-flake rules, and the SDLC-phase gating). This skill covers what's specific to visualization code on top of that: data-frame/panel-prop builders, the canvas draw-call snapshot harness, panel accessibility and interaction-snapshot E2E, and canvas/uPlot-specific anti-flake rules. The visualization codeowner paths are opted into the gating `check-frontend-test-coverage.yml` check, so coverage that drops fails CI.
Step 1 — Set up data with the repo's builders
Build data frames with the `@grafana/data` builders — **pick one and don't mix** `toDataFrame` and `createDataFrame` in the same file:
import { createDataFrame, toDataFrame, arrayToDataFrame, FieldType, LoadingState } from '@grafana/data';Use a **single canonical builder per file** with a `Partial<>` overrides object, rather than bespoke frames per test:
function makeFrame(overrides: Partial<Options> = {}) {
/* … */
}To render a panel component, use the shared panel-props builder instead of hand-rolling props:
import { getPanelProps } from '../test-utils'; // public/app/plugins/panel/test-utils.ts
render(<BarChartPanel {...getPanelProps(defaultOptions, { fieldConfig })} />);> **Gotcha — field config.** A panel unit test must call `applyFieldOverrides` itself with a > `createFieldConfigRegistry`; the panel framework normally does this, so without it your > custom `fieldConfig.custom` never reaches the render and every case looks identical.
> **Gotcha — type inference.** If you're testing `guessFieldTypes` (or any inference), feed > **untyped** raw fields (`as unknown as DataFrameDTO`). `createDataFrame` pre-sets `type`, so > the function under test becomes a no-op and the test gives false confidence.
Step 2 — HTML5 canvas / rendering panels: use the draw-call snapshot harness
Panels that draw to canvas (timeseries, heatmap, xychart, timeline, piechart, sparkline) are tested by **capturing the ctx draw-call stream**, not by pixel-diffing. Follow the established harness:
// In the harness (public/app/plugins/panel/timeseries/TimeSeriesPanel.canvasTestUtils.tsx):
import {
applyDefaultUPlotAxisMeasureTextMock,
installCanvasPath2DShim,
removeCanvasTransforms,
} from '@grafana/test-utils/canvas';
// In each *.canvas.test.tsx, mock grafana-ui's text measurement so layout is deterministic:
jest.mock('@grafana/ui/src/utils/measureText', () =>
require('@grafana/test-utils/canvas').createGrafanaUiMeasureTextJestMock(() =>
require('./TimeSeriesPanel.canvasTestUtils').getUPlotInstance()
)
);- Split suites by concern: `*.lines.canvas.test.tsx`, `*.fills.…`, `*.annotations.…`,
`*.axisPlacement.…`, `*.axisRange.…` — each a focused `it.each` of cases.
- Assert with the custom matcher: `expect(events).toMatchCanvasSnapshot(context, { width, height })`.
- **Keep it deterministic** (this is where flake comes from): fixed `width`/`height`, UTC
timestamps (`Date.UTC(...)`, `timeZone: 'utc'`), and wait for the renderer to be ready before asserting — `await waitFor(() => expect(uPlotInstance?.status).toBe(1))` (a `waitFor` callback must **throw** to retry, so it needs `expect`, not a bare boolean).
Step 3 — E2E for interaction, accessibility, and interaction snapshots
The DataViz strategy is **unit-first**. Reserve Playwright for cross-component interaction and per-panel smoke coverage. When you do write E2E:
- Add the selector to the **versioned `@grafana/e2e-selectors` package first**, wire
`data-testid` into the JSX, then query it (use the `add-e2e-selectors` skill).
- Query by selector, never brittle CSS — in E2E `dashboardPage.getByGrafanaSelector(...)`,
in unit `screen.getByTestId(selectors.components...)`.
import { test, expect } from '@grafana/plugin-e2e';
test.describe('Panels test: BarChart render', { tag: ['@panels', '@barchart'] }, () => {
test('renders without error', async ({ gotoDashboardPage, selectors }) => {
const page = await gotoDashboardPage({ uid: DASHBOARD_UID }); // provisioned devenv dashboard
await expect(page.getByGrafanaSelector(selectors.components.Panels.Panel.headerCornerInfo('error'))).toBeHidden();
});
});Accessibility — every panel gets an a11y check
**Every panel must have an E2E accessibility test.** Use the `scanForA11yViolations` fixture and the `toHaveNoA11yViolations()` matcher, in a `describe`/test tagged `@a11y`. Load the panel, wait for it to actually render (assert the panel title and the chart element are visible — an empty panel trivially passes), then scan:
test.describe('a11y', { tag: ['@a11y'] }, () => {
test('run a11y report', async ({ gotoDashboardPage, scanForA11yViolations, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ viewPanel: 'panel-4' }),
});
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('…'))).toBeVisible();
await expect(page.locator('.uplot')).toBeVisible(); // panel has drawn
const report = await scanForA11yViolations({
options: { runOnly: ['wcag2a', 'wcag2aa', 'wcag21a', 'The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
Other skills on grafana.
More skills from other plugins.
api-review
Produce the "Playwright 1.xx API Review" artifact for a range of commits - the public API additions and the breaking or behavior…
promptfoo-evals
Write, refine, run, and QA promptfoo evaluation suites: promptfooconfig.yaml, prompts, providers, vars, tests, assertions,…
deepeval
DeepEval evaluation workflow for AI agents and LLM applications. TRIGGER when the user wants to evaluate or improve an AI agent,…
dev
Development workflows for the playwright-cli repository. Use when the user asks about rolling dependencies, releasing, or other…
Questions about panel-testing-strategy.
- Which coding agents can use panel-testing-strategy?
- panel-testing-strategy installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

