/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,
$ npx -y skills add grafana/grafana --skill panel-testing-strategy --agent claude-codeHow 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.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
Write tests for Grafana visualization code that pass review on the first pass. Goals: **assert concrete behavior, not existence**; keep test descriptions honest; verify the test actually exercises the target code path; use the repo's data-frame and panel-props builders; snapshot HTML5 canvas based panels via the draw-call harness; and stabilize the known flake classes. The visualization codeowner paths are opted into the gating `check-frontend-test-coverage.yml` check, so coverage that drops fails CI.
Resolve the target
Interpret the argument to decide scope:
- **A file path** → test that file (create or extend its co-located `*.test.ts(x)`).
- **A directory / panel name** → the source files under it lacking meaningful coverage.
- **"current file" / no path but a file is open** → the open file.
- **No argument** → ask which panel/area; don't blanket-generate.
Prefer extending an existing co-located test file over adding a new one. Match the surrounding test file's imports and idiom.
Principle 1 — Where each test fits: the (inverted) testing diamond
Testing model, top to bottom:
- **E2E** (pinnacle) — validate the system via real user flows; powerful but slow, so keep
it targeted and few.
- **Unit** (base) — cheap, plentiful specs documenting behavior for logic/utils.
- **Static analysis** (foundation) — lint + strong TypeScript interfaces.
Pick the layer that matches the job: logic/IO → unit; how pieces fit together → integration/visual; a key user journey → E2E. **Favour speed and feedback** — unit tests are cheap, so make them small and plentiful; reserve the expensive layers for what only they can cover. (Steps 1–4 below are the unit craft; Step 5 is E2E.)
Principle 2 — Assert real behavior, not existence
This is the bar reviewers hold every test to, at every layer. They reject tests that only prove a function ran. Never land these as the whole test:
expect(result).toBeDefined(); // ❌ proves nothing about correctness
expect(result).toBeInstanceOf(Foo); // ❌ (unless the type itself is the contract)
expect(() => fn(input)).not.toThrow(); // ❌ "didn't crash" is not a behavior
expect(result).toHaveLength(input.length); // ❌ if it just mirrors the input
Instead assert the **concrete computed value**, so a failure points at the real bug:
// diffperc: 10 -> 20 is a +100% change
const results = getDisplayValuesForCalcs(/* … */);
expect(results[0].numeric).toBe(100); // ✅ assert the math
expect(results[0].text).toBe('100%'); // (formatting is secondary)If the function mostly delegates, assert the delegation with exact arguments (see Step 3).
**Expected values are literals, not recomputations.** Never derive the expected side by calling the code under test, a collaborator it calls internally, or by re-typing the production formula — the test then passes whenever the code and the expectation share the same bug, and comparing a value to _itself_ asserts nothing at all. Freeze the expected value as a literal, computed once by hand or captured from a known-good run:
// ❌ circular: `expected` is produced the same way the code produces its result
const expected = theme.visualization.getColorByName('red');
expect(dim.value()).toBe(expected);
// ❌ re-derives the production formula — a bug in the formula is copied into `expected`
const expected = TABLE.CELL_PADDING * 2 + theme.typography.fontSize * theme.typography.body.lineHeight;
expect(getDefaultRowHeight(theme, [])).toBe(expected);
// ✅ frozen literals — a change in the resolver or the formula now fails the test
expect(dim.value()).toBe('#F2495C');
expect(getDefaultRowHeight(theme, [])).toBe(34);For values awkward to write by hand (projected coordinates, hashes), assert an **independent readback** rather than re-running the same path — e.g. project lng/lat, read it back in WGS84, and compare to the literal input — or freeze it with `toMatchInlineSnapshot`.
**Prove the assertion has teeth.** Before landing, mutate the asserted value (or the source it derives from) and confirm the test goes **red**. A test that stays green — because its expectation tracks the code, or checks a value against itself — is a tautology dressed as coverage. Make this a habit, not just the final Verify step.
Principle 3 — Authoring with AI: no slop tests
This skill exists so AI-proposed tests meet the bar above. The failure mode to avoid is the **slop test**:
- **Unfocused** — a wide blast of assertions that doesn't preserve the intent of the code
under test.
- **Verbose** — unnecessary steps/mocks for a simple goal; brittle to implementation
changes, and can silently mask real regressions.
- **Limiting** — so many, or so coupled to implementation, that a later refactor breaks them
without telling you whether behavior actually broke. (Unreadable DOM snapshot tests are the classic example — never add them.)
Review AI output _thoroughly_ before opening a PR; expect to amend it for readability/maintainability. If reviewing the AI output costs more than writing the test by hand, write it by hand. A test is a specification a teammate — and future-you — must read easily; value refactoring for readability over a raw coverage percentage.
Principle 4 - Do not simply update failing tests to pass after changing behaviour, or adding a feature
When a test fails after updating functionality, behaviour or features, this is a warning that a regression was cau
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
Write tests for Grafana visualization code that pass review on the first pass. Goals: **assert concrete behavior, not existence**; keep test descriptions honest; verify the test actually exercises the target code path; use the repo's data-frame and panel-props builders; snapshot HTML5 canvas based panels via the draw-call harness; and stabilize the known flake classes. The visualization codeowner paths are opted into the gating `check-frontend-test-coverage.yml` check, so coverage that drops fails CI.
Resolve the target
Interpret the argument to decide scope:
- **A file path** → test that file (create or extend its co-located `*.test.ts(x)`).
- **A directory / panel name** → the source files under it lacking meaningful coverage.
- **"current file" / no path but a file is open** → the open file.
- **No argument** → ask which panel/area; don't blanket-generate.
Prefer extending an existing co-located test file over adding a new one. Match the surrounding test file's imports and idiom.
Principle 1 — Where each test fits: the (inverted) testing diamond
Testing model, top to bottom:
- **E2E** (pinnacle) — validate the system via real user flows; powerful but slow, so keep
it targeted and few.
- **Unit** (base) — cheap, plentiful specs documenting behavior for logic/utils.
- **Static analysis** (foundation) — lint + strong TypeScript interfaces.
Pick the layer that matches the job: logic/IO → unit; how pieces fit together → integration/visual; a key user journey → E2E. **Favour speed and feedback** — unit tests are cheap, so make them small and plentiful; reserve the expensive layers for what only they can cover. (Steps 1–4 below are the unit craft; Step 5 is E2E.)
Principle 2 — Assert real behavior, not existence
This is the bar reviewers hold every test to, at every layer. They reject tests that only prove a function ran. Never land these as the whole test:
expect(result).toBeDefined(); // ❌ proves nothing about correctness expect(result).toBeInstanceOf(Foo); // ❌ (unless the type itself is the contract) expect(() => fn(input)).not.toThrow(); // ❌ "didn't crash" is not a behavior expect(result).toHaveLength(input.length); // ❌ if it just mirrors the input
Instead assert the **concrete computed value**, so a failure points at the real bug:
// diffperc: 10 -> 20 is a +100% change
const results = getDisplayValuesForCalcs(/* … */);
expect(results[0].numeric).toBe(100); // ✅ assert the math
expect(results[0].text).toBe('100%'); // (formatting is secondary)If the function mostly delegates, assert the delegation with exact arguments (see Step 3).
**Expected values are literals, not recomputations.** Never derive the expected side by calling the code under test, a collaborator it calls internally, or by re-typing the production formula — the test then passes whenever the code and the expectation share the same bug, and comparing a value to _itself_ asserts nothing at all. Freeze the expected value as a literal, computed once by hand or captured from a known-good run:
// ❌ circular: `expected` is produced the same way the code produces its result
const expected = theme.visualization.getColorByName('red');
expect(dim.value()).toBe(expected);
// ❌ re-derives the production formula — a bug in the formula is copied into `expected`
const expected = TABLE.CELL_PADDING * 2 + theme.typography.fontSize * theme.typography.body.lineHeight;
expect(getDefaultRowHeight(theme, [])).toBe(expected);
// ✅ frozen literals — a change in the resolver or the formula now fails the test
expect(dim.value()).toBe('#F2495C');
expect(getDefaultRowHeight(theme, [])).toBe(34);For values awkward to write by hand (projected coordinates, hashes), assert an **independent readback** rather than re-running the same path — e.g. project lng/lat, read it back in WGS84, and compare to the literal input — or freeze it with `toMatchInlineSnapshot`.
**Prove the assertion has teeth.** Before landing, mutate the asserted value (or the source it derives from) and confirm the test goes **red**. A test that stays green — because its expectation tracks the code, or checks a value against itself — is a tautology dressed as coverage. Make this a habit, not just the final Verify step.
Principle 3 — Authoring with AI: no slop tests
This skill exists so AI-proposed tests meet the bar above. The failure mode to avoid is the **slop test**:
- **Unfocused** — a wide blast of assertions that doesn't preserve the intent of the code
under test.
- **Verbose** — unnecessary steps/mocks for a simple goal; brittle to implementation
changes, and can silently mask real regressions.
- **Limiting** — so many, or so coupled to implementation, that a later refactor breaks them
without telling you whether behavior actually broke. (Unreadable DOM snapshot tests are the classic example — never add them.)
Review AI output _thoroughly_ before opening a PR; expect to amend it for readability/maintainability. If reviewing the AI output costs more than writing the test by hand, write it by hand. A test is a specification a teammate — and future-you — must read easily; value refactoring for readability over a raw coverage percentage.
Principle 4 - Do not simply update failing tests to pass after changing behaviour, or adding a feature
When a test fails after updating functionality, behaviour or features, this is a warning that a regression was cau
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.
Repo: grafana/grafana

