name: add-e2e-selectors description: Add reliable @grafana/e2e-selectors to interactive elements and key containers in the Grafana frontend. Use when adding e2e selectors, data-testid attributes, or test selectors to React components, when a file or component lacks selectors for testing, or when asked to make elements testable. Also use when given a Grafana Pathfinder interactive guide (a guide directory or content.json) to audit or fix — it extracts the guide's reftarget selectors, identifies weak ones, and fixes them at the source in Grafana's JSX. Defines versioned selectors in the e2e-selectors package and wires data-testid into JSX. Accepts a file path, a list of targets, a directory, a pathfinder guide, or the current/open file.
Add e2e-selectors
Find interactive elements and key containers in the Grafana frontend that lack a stable test
selector, define a versioned selector in the @grafana/e2e-selectors package, and wire it
into the JSX as data-testid. This encodes the package's layout (pages vs components), its
semver versioning scheme, and its strict reuse / never-delete rules so selectors are added
correctly and don't break plugin end-to-end tests.
Resolve the target
Interpret the argument to decide scope:
- A file path → only that file.
- A list of files or described elements → each named target, one by one.
- A directory → all
.tsxfiles under it. - A Grafana Pathfinder interactive guide — a guide directory or its
content.json(e.g. undergrafana-pathfinder-app/src/bundled-interactives/) → see "Pathfinder guide as target". - "current file" / "open file" / no path but a file is open → the open file.
- No argument and no open file → ask for a target; never scan the whole frontend.
Pathfinder guide as target
Interactive guides drive Grafana's UI through CSS selectors (reftarget fields), so a weak
selector breaks a guide the same way it breaks a test. Process the guide, then fix the weak
targets at the source:
- Extract targets:
grep -o '"reftarget": *"[^"]*"' <guide>/content.json - Skip navigation targets — blocks with
"action": "navigate"or URL-shaped values (starting with/orhttp). - Classify the rest. Strong (leave alone):
data-testid/data-cy-based selectors andgrafana:/{grafana:...}tokens. Weak (fix): text matching (:contains,:text),aria-label/placeholder/titleattributes,href, bare-id compounds, and positional selectors (:nth-of-type,:nth-child,:nth-match). - Locate the JSX in this repo rendering each weak target (search by id, testid fragment,
button text, aria-label). Some targets are not fixable here — external npm packages
(
@grafana/plugin-ui,@grafana/prometheus), Monaco editor internals, instance data (dashboard titles, datasource names) — report those instead of forcing a change. - Run Steps 1–6 below for the located elements. Updating the guide itself is out of scope — include a weak-selector → new-selector mapping in your summary so the guide author can adopt them.
Step 1 — Find the version key
All new selectors added in this run use a single version key: the current main version with
-pre and build tags stripped.
grep -m1 '"version"' package.json
13.2.0-pre → use '13.2.0'. On a release branch, read main's copy instead
(git show main:package.json | grep -m1 '"version"' — the local main ref can be stale, so
fetch first if in doubt). If you know the change will be backported, use the lowest release
version instead. Never hardcode — always compute it.
Step 2 — Identify elements
Target these in the file:
- Interactive controls a test would click or type into:
button,input,select,textarea,a, links, toggles/switches, checkboxes, radios, menu items, tabs, and the grafana-ui components that render them (Button,IconButton,Input,Select,Switch,Checkbox,Tab, etc.). - Key containers tests scope queries to: modals, panels, page sections, dialogs.
Skip any element that already has a data-testid or an existing selector — don't duplicate.
Exception — a static testid on a repeated item. A hardcoded literal inside a .map()
(every card rendering the same data-testid="data-source-card") identifies nothing: consumers
are forced into text matching or positional hacks to pick one item, so it's as weak as no
selector at all. Migrate it:
-
Define a parameterized selector keyed by a stable per-item value.
-
Preserve the legacy literal as the
MIN_GRAFANA_VERSIONentry so version-resolved consumers keep working against older Grafana, keeping the signature compatible:dataSourceCard: { '13.2.0': (name: string) => `data-testid data source card ${name}`, [MIN_GRAFANA_VERSION]: (_name: string) => 'data-source-card', }, -
Update every in-repo usage of the old literal (jest and Playwright — grep
public/ande2e-playwright/), and note in your summary that external code hardcoding the literal will need the same one-line update.
Loop rule (important)
Inside a .map() / list render, do not put a selector on each inner interactive element —
that bloats the DOM and degrades render performance. Instead attach one parameterized
container selector to the repeated row/wrapper, keyed by a unique value. Tests then scope
their queries within the matched row.
Canonical example —
public/app/features/browse-dashboards/components/DashboardsTree.tsx:
<div
key={key}
{...rowProps}
data-testid={selectors.pages.BrowseDashboards.table.row(
'title' in dashboardItem ? dashboardItem.title : dashboardItem.uid
)}
>
{row.cells.map((cell) => /* inner cells get NO individual selector */)}
</div>
Step 3 — Reuse check (never duplicate)
Before defining anything, search the package for an existing selector covering this UI:
grep -rn "<keyword>" packages/grafana-e2e-selectors/src/selectors/components.ts packages/grafana-e2e-selectors/src/selectors/pages.ts
If you're modifying UI that already has a selector, reuse it — creating a new one breaks plugin e2e tests. Only create a new selector for genuinely new UI.
Two special cases the grep can surface:
- Dormant entry — defined in the package but never wired into JSX (grep its value across
public/andpackages/): wire it instead of creating a parallel one, adding adata-testid-prefixed version key first if the existing value is un-prefixed. - Orphaned entry — the UI it tagged has been deleted: leave the entry in place (never delete) and mention it in your summary.
Step 4 — Confirm the element accepts a selector prop
Before defining a selector for an element, confirm the target can actually receive it:
- Plain DOM elements (
div,button,input,a, …) always acceptdata-testid— proceed. - grafana-ui / React components only accept it if the component is written to forward it.
Open the component and check that it either spreads remaining props onto the rendered DOM
(
{...rest}/{...otherProps}extending anHTMLAttributestype) or exposes a dedicated prop for the test id (e.g. grafana-ui'sMenu.Itemuses atestIdprop, notdata-testid). Use whichever the component actually supports. Known cases, to save a lookup (the source is still authoritative if in doubt):Button,Input,FilterInput, andToolbarButtonforward rest props, so plaindata-testidworks;CardspreadshtmlPropsonto its container div;Menu.ItemtakestestId.
If the component accepts neither data-testid nor an equivalent prop, stop for that
element and surface it to the user — name the component, the file, and that it doesn't forward
a test id. Do not add a new prop, spread, or otherwise modify an existing component to make
it accept one. Move on to the remaining elements and report the skipped one in your summary.
Step 5 — Define the selector
- Where:
packages/grafana-e2e-selectors/src/selectors/components.tsif the element is rendered on more than one route or ships in grafana-ui;pages.tsif it's tied to a single route/screen (URLs also live there). - Group: nest under an existing group that mirrors the UI hierarchy, or add a new group named after the component/page. Place a new group next to related groups — the files are not alphabetical.
- Shape: a versioned object whose key is the version from Step 1 and whose value is
prefixed
data-testid(the prefix tells the framework to match thedata-testidattribute rather than an aria-label):
MyComponent: {
submitButton: {
'13.2.0': 'data-testid MyComponent submit button',
},
},
-
Key naming. Keys form a public API plugins depend on, so name them deliberately:
- Casing: use PascalCase for a group key that names a distinct UI unit — a component,
form, modal, or drawer (
NewFolderForm,MoveModal,CreateNewButton); mirror the component name. Use camelCase for conceptual/functional groups that aren't a single named component (table,actions,emptyState) and for every leaf element key (submitButton,searchInput,selectAllCheckbox). - Name leaf keys by role, not by visible label —
moveButton, nevermoveorMove. This survives copy changes and reads unambiguously as a control. - The markup drives the name — check the element itself, not its neighbours. The role
encoded in the key must match what the element actually renders. Use
…Buttononly for something that renders/behaves as a button (a<button>,Button, or aMenuItemwithonClickand nourl); use…Linkfor something that navigates (an<a>,LinkButton, orMenuItemwithurl/href); use the matching role for inputs, checkboxes, etc. A suffix that contradicts the markup — or one carried over from a differently-rendered element nearby — is the defect. A single menu can legitimately contain both buttons and links, so their keys should differ (newDashboardLinknext tonewFolderButton); that is correct, not an inconsistency to flatten. - Same markup ⇒ same name, everywhere in the run — not just within one group. Two controls
with the same markup/role are the same kind of thing and must be named identically wherever
they appear, across sibling keys and across groups. Two dropdown-trigger buttons must both be
triggerButton— nottriggerButtonin one group andbutton(orcreateNewButton) in another; two actionMenuItems must both be…Button— notmoveButtonnext to baremanagePermissions. The usual cause of a violation is naming each group in isolation and reaching for whatever reads locally; before finalizing, look across the whole file for other instances of the same control and reuse that name. - Key ↔ value agreement. The role word in the key must match the tail of its value: key
moveButton⇒ value… move-button; keymove⇒ value… move. A mismatch (key saysmoveButton, value ends…-move) is a reliable signal the key is wrong — reconcile them.
- Casing: use PascalCase for a group key that names a distinct UI unit — a component,
form, modal, or drawer (
-
Prefer string selectors. Use a function selector only for genuinely parametric IDs (loop/row keys, dashboard UIDs):
table: {
row: {
'13.2.0': (id: string) => `data-testid BrowseDashboards table row ${id}`,
},
},
Parameterize by a stable value (a uid, refId, or from/to token), never by display
text — translated or user-editable text reintroduces the i18n fragility the selector exists
to remove. Prefer a single parameter: some consumers (e.g. Pathfinder {grafana:path:param}
tokens) can only pass one. When adding a version key to a function selector, keep the
signature compatible across all keys (see the migration example in Step 2).
-
Upgrading legacy aria-label entries. A value without the
data-testidprefix tells the framework to matcharia-labelinstead. Don't add aria-labels to JSX just to satisfy such an entry — the upgrade path is to add a new prefixed version key to the existing entry and wiredata-testidin the JSX. Keep an aria-label only where it carries genuine accessibility value (see "Aria-Labels vs data-testid" incontribute/style-guides/e2e-playwright.md). -
Never edit or delete an existing entry. To change an existing selector's value, add a new version key alongside the old one and keep the signature backwards compatible.
-
Ensure each new testid value is unique. The string value (the part after the
data-testidprefix) must not already exist anywhere in the package, or tests will match the wrong element. Check before committing to a value:grep -rn "MyComponent submit button" packages/grafana-e2e-selectors/src/selectors/Expect zero matches other than the entry you just added. Also confirm the new values are unique against each other within this run. If a value collides, pick a more specific one (include the component/page name and the element's role).
-
Check for in-flight collisions.
components.tsandpages.tsare hot files — someone may be adding selectors for the same UI right now. Before finalizing group names and values, scan open PRs touching them:gh pr list --search "e2e-selectors" --state open. Always avoid group names and values those PRs introduce. What to do about overlapping UI depends on why you're touching it:- The element was explicitly requested (named in the task, or a weak guide target you were asked to fix): do the work anyway and surface the overlap in your summary — an open draft PR is not a reason to silently return nothing the user asked for. If the PR's approach conflicts with this skill's rules (e.g. it satisfies a legacy entry with an aria-label), say so; the user decides which lands.
- You found the element during open-ended discovery (directory sweep, guide scan choosing among many candidates): skip UI an open PR already covers and spend the effort on uncovered targets instead.
Step 6 — Apply in JSX
Ensure the import exists (add it if missing):
import { selectors } from '@grafana/e2e-selectors';
Then wire the attribute:
// static control
<Button data-testid={selectors.components.MyComponent.submitButton} onClick={onSubmit}>
Save
</Button>
// parameterized loop-row container
<div data-testid={selectors.pages.BrowseDashboards.table.row(item.uid)} />
Many grafana-ui components forward a data-testid prop, so passing it directly works; for
plain DOM elements set the attribute literally.
Examples
// Static selector on a control — Drawer close button
<IconButton data-testid={selectors.components.Drawer.General.close} onClick={onClose} />
// Parameterized selector for a repeated item — Tab title
<button data-testid={selectors.components.Tab.title(label)}>{label}</button>
// Page-level container selector
<div data-testid={selectors.pages.Explore.General.container}>{children}</div>
Rules checklist
- Never delete a selector — external plugins depend on them.
- Reuse the existing selector when touching UI that already has one; only create for new UI.
- Confirm the element accepts
data-testidor an equivalent prop before defining a selector; if it doesn't, surface it to the user and never add a prop to an existing component. - Every new testid value must be unique across the package and within this run.
- Prefer string selectors; function selectors only for parametric loop/row/UID values.
- Name a key's role to match the element's actual markup (
…Buttonfor buttons,…Linkfor navigation, etc.) — never carry it over from a differently-rendered neighbour; keep the key's role word in sync with its value. - Same markup ⇒ same name everywhere in the run — name equivalent controls identically across
sibling keys and across groups (e.g. every dropdown trigger is
triggerButton); scan the whole file for existing instances before naming a new one. - New selectors use the version key from Step 1 (no
-pre/build tags). - To change a value, add a new version key — don't edit the old value or change the signature.
- In loops, selector goes on the row/container, not each inner element.
- A static testid on a repeated item is as weak as none — migrate it to a parameterized
selector, preserving the legacy literal as the
MIN_GRAFANA_VERSIONentry and updating every in-repo usage of the old literal. - Wire dormant (defined-but-never-used) entries instead of creating parallel ones; report orphaned entries, never delete them.
- Function-selector parameters are stable values (uid, refId, tokens), never display text; prefer a single parameter.
See packages/grafana-e2e-selectors/src/selectors/README.md and
contribute/style-guides/e2e-playwright.md for the authoritative guidance.
Verify
yarn typecheck— confirms the selector path and any function signature resolve. This runs the whole monorepo and takes several minutes; that's expected.- If a literal testid was replaced (Step 2 exception), grep the old literal across
public/ande2e-playwright/— expect zero remaining references. - Selectors are resolved at runtime by the package's resolver; no codegen step is needed
after editing
components.ts/pages.ts. yarn lintthe changed files — theimport/orderrule cares where the new@grafana/e2e-selectorsimport lands (alphabetical within the@grafana/*group).
TDD Red-Green-Refactor
Testing
Skill qui guide Claude a travers le cycle TDD complet.
Audit d'Accessibilité Web
Testing
Réalise un audit d'accessibilité web complet selon les normes WCAG.
Générateur de Tests UAT
Testing
Génère des cas de test d'acceptation utilisateur structurés et complets.