Skip to content
Testing
Skill

/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,

From plugin
grafana
77k3 skills
Install
$ npx -y skills add grafana/grafana --skill panel-testing-strategy --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/panel-testing-strategy

Context preview

The summary Claude sees to decide when to auto-load this skill.

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,

SKILL.md

panel-testing-strategy.SKILL.md
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', '
Read more
Ships withgrafana

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.

Get the whole plugin
Stats
76,747
Stars
14,734
Forks
Active
Maintenance
TypeScript
Language
AGPL-3.0
License
1h ago
Last commit
12y ago
Created

Repo: grafana/grafana

Other skills on grafana.