Troubleshooting UI Coverage
UI Coverage produces a report for every recorded run with no setup, and for most applications the defaults are accurate on their own. When a report doesn't match what you expect, the cause is usually one of a small, predictable set: an unstable attribute that splits one control into many, a generic identifier that merges distinct controls, an interaction UI Coverage doesn't recognize, or a configuration change that hasn't been applied to the run yet.
This page maps each symptom to its cause and the configuration that resolves it. Because you can regenerate any past run with your current configuration, you can confirm every fix in minutes without re-running your Cypress tests, so the fastest path to a coverage score you trust is to make one change, regenerate a recent run, and check the result.
Every run is processed with the configuration that was saved at that moment, so edits you make afterward don't change an existing report until you reprocess it. This timing is the single most common reason a rule "doesn't work."
After saving configuration in the App Quality tab of your project settings, open a run's Properties tab and use its "regenerate" button, or the "configuration updated" message shown on a report processed with older configuration. Every fix on this page assumes you regenerate the run after changing configuration. See Setting configuration.
One element is reported as many
A single control appears as several separate untested elements, inflating your element count and lowering your score.
Cause. UI Coverage recognizes the same element across snapshots by building an identity from its attributes and position in the DOM. When an identifying attribute changes on every render, such as a framework-generated id, a hashed class, or a per-request token, each snapshot looks like a new element and the one control is split apart.
UI Coverage already ignores the most common unstable values, including UUIDs and long hexadecimal hashes on any attribute, digit-only id, name, for, and aria-* values, and generated class names like jss* and ng-tns-*. Add configuration only when your application produces unstable identifiers these built-in filters don't cover.
Fixes.
-
Ignore the dynamic attribute with
attributeFiltersso UI Coverage falls back to a stable attribute. When you filter a dynamicid, also filter the attributes that reference it, such asfor,aria-labelledby, andaria-describedby, or the element can still be identified by those related values.App Quality Config{"attributeFilters": [{"attribute": "id|for|aria-labelledby|aria-describedby","value": ":r.*","include": false,"comment": "React useId() values like :r0: are regenerated on every render"}]} -
Pin one specific element's identity with an
elementsrule that identifies it by a selector that stays the same across snapshots. This is the right tool for a single control; if many elements share the same generated attribute, filter the attribute instead.App Quality Config{"uiCoverage": {"elements": [{"selector": "#checkout-form [id^='card-']","name": "Card Number Field"}]}}
Patterns in attributeFilters match the whole value (they're anchored as ^(...)$, unless your pattern already starts with ^ or ends with $), and JSON requires backslashes to be doubled, so a token like \d must be written \\d. Note that attribute is matched case-insensitively while value is matched case-sensitively. See How rules are applied.
Distinct elements are reported as one
Two or more controls that should each be tracked appear as a single element, so testing one marks all of them tested and real gaps are hidden.
Cause. The elements resolve to the same identity because their identifying attribute shares a generic value, such as several buttons that all carry data-test="button", or because the attribute that would distinguish them isn't one UI Coverage uses by default.
Fixes.
-
Add distinct identifiers in your application, giving each control its own
data-cyordata-testvalue. This is the most durable fix. -
Prefer a distinguishing attribute your markup already has with
significantAttributes. Attributes you list are checked before the defaults, so an attribute whose value differs between the elements gives each a distinct identity. Icon-only controls, for example, are often distinguishable by theiraria-label.App Quality Config{"significantAttributes": ["aria-label"]}Listing
aria-labelalso turns the report into an inventory of your label text, where a vague label that passes automated accessibility checks (aria-label="button") stands out next to a clear one (aria-label="Close dialog").
Similar elements aren't grouped together
Repeated components, such as a row action rendered for every record, appear as many separate untested elements instead of one group, so a single table can dominate your untested list.
Cause. Each instance carries a per-instance value in its identifier, such as a database ID in an id attribute, so UI Coverage treats the instances as unrelated rather than as one repeated component.
Fixes.
-
Ignore the per-instance value with
attributeFiltersso the otherwise-identical elements collapse into one group on their own.App Quality Config{"attributeFilters": [{"attribute": "id","value": "delete-user-\\d+","include": false,"comment": "Row action ids include the per-user database id"}]} -
Force the grouping with an
elementGroupsrule that matches all the instances by a shared selector and names them, which also makes the report readable.App Quality Config{"uiCoverage": {"elementGroups": [{"selector": "[id^='delete-user-']","name": "Delete User Button"}]}} -
Group in your markup by adding the
data-cy-ui-groupattribute to the elements, or to a wrapper around them, when the team that owns the markup also owns the grouping decision. Groups defined this way take priority overelementGroupsconfiguration.
Unrelated elements are grouped together
Controls that do different things are combined into one group, so an interaction with one wrongly marks the others tested.
Cause. The elements share an overly generic selector or attribute value, so UI Coverage's automatic grouping or a shared identifying attribute treats them as instances of the same component.
Fixes.
-
Add distinct identifiers so each control resolves to its own identity, which prevents the shared-attribute grouping in the first place.
-
Define the correct split with
elementGroups. A custom group overrides the automatic grouping, so a rule for each action separates controls the heuristics lumped together. List specific rules before broad ones, because the first matching rule wins.App Quality Config{"uiCoverage": {"elementGroups": [{"selector": "[data-cy='quick-view']","name": "Quick View Button"},{"selector": "[data-cy='add-to-cart']","name": "Add to Cart Button"}]}}
An element you tested is shown as untested
Your test interacts with an element on every run, but the report still lists it as untested.
Cause. UI Coverage marks an element tested only when a recognized interaction command targets it. The built-in set is blur, check, clear, click, dblclick, focus, rightclick, scrollIntoView, scrollTo, select, selectFile, submit, trigger, type, and uncheck. Anything outside that set, including your own custom commands and plugin commands such as cypress-real-events' realClick and realHover, doesn't count until you declare it.
Cause: you used scrollIntoView. That command credits the element's nearest scrollable ancestor rather than the element you called it on, so it usually marks nothing tested. Use click, focus, or trigger on the element instead.
Fixes.
-
Count a custom or plugin command everywhere by adding it to
additionalInteractionCommands.App Quality Config{"uiCoverage": {"additionalInteractionCommands": ["realClick", "realHover"]}} -
Count a command for specific elements, or credit an interaction the defaults ignore such as an
asserton a read-only badge or total, withallowedInteractionCommands.App Quality Config{"uiCoverage": {"allowedInteractionCommands": [{"selector": "[data-cy='order-total']","commands": ["assert"],"comment": "This total is only ever validated, never interacted with"}]}}
Two details often explain a command that still doesn't count. First, a custom command only produces coverage if it logs a snapshot that references the element it acts on, so register it with prevSubject and log the subject as $el (see Requirements for a custom command). Second, command names you add are matched case-sensitively, exactly as you registered them.
An element stops counting after you add an allowedInteractionCommands rule
An element that was tested is now reported as untested, right after you added an allowedInteractionCommands rule.
Cause. Once an element matches a rule, only the commands listed in the matching rules count for it, and the default commands no longer apply. If your test exercises the element with a command you didn't list, such as a plain click, it's now treated as untested.
Fix. Add the command your test actually uses to the rule's commands list, or tighten the selector so the rule matches only the elements you intended to restrict.
A configuration rule has no effect
You saved a rule, but the report looks the same.
Work through these causes in order:
- The run predates the change. Reports use the configuration saved when they were processed. Regenerate the run to apply your current configuration. This is by far the most common cause.
- An earlier rule matched first. For
elementFilters,viewFilters,elementGroups, andattributeFilters, the first matching rule wins, so a broad rule above your specific one prevents it from applying. List specific rules first, and placeinclude: trueexceptions before theinclude: falserule they should override. (elementsis the exception: when several rules match one element, the last one wins.) - A nested list replaced a root one. Nesting a shared property such as
elementFiltersorattributeFiltersunder auiCoverageoraccessibilitykey completely replaces the root-level list for that product; the two are never merged. Repeat any root rules you still want in the nested list. See Configuration scope. - The property is in the wrong place.
elementGroups,elements,additionalInteractionCommands, andallowedInteractionCommandsare UI Coverage–only and must live under theuiCoveragekey, whileviewsis set at the root. A misplaced property is rejected by the schema. - The pattern or selector doesn't match.
attributeFilterspatterns are anchored to the whole value, sovalue: "user"matches only the exact stringuser; usevalue: "user-.*"for a prefix. Selectors inelementFilters,elements, andelementGroupsmust match the interactive element itself, not a wrapper around it, sofootermatches only the<footer>, whilefooter *matches the controls inside it. - The configuration didn't save. Cypress Cloud validates against a strict schema and rejects any property it doesn't recognize, including a misspelled name, a value of the wrong type, or a freeform note on a field that doesn't accept one. Keep notes in a
comment, and correct the flagged property so the configuration can save.
The coverage score looks wrong
Your score is lower than the state of your tests suggests.
Cause. The score compares tested interactive elements to the total, and a few sources of untested elements commonly drag it down without pointing to a real gap in your suite:
- Third-party widgets. Chat launchers, cookie banners, and analytics overlays are interactive elements your team doesn't own, and each counts as untested.
- Untested links. A link to a page no test visits, including your own help center, marketing pages, and entirely external sites, counts against your score as an untested link, even though no view exists for it.
- Duplicated elements. One control split into many by an unstable attribute (see One element is reported as many) adds untested elements that are really the same control.
Fixes.
-
Remove third-party elements with
elementFilters, which drops them from the report and the score.App Quality Config{"elementFilters": [{"selector": ".intercom-launcher","include": false,"comment": "Third-party chat widget, not part of our tested UI"}]} -
Exclude destinations you don't intend to test with
viewFilters, which also removes the links that point to them.App Quality Config{"viewFilters": [{"pattern": "https://status.my-app.com/*","include": false,"comment": "External status page, linked from the footer but out of scope"}]}
To keep only your own application's URLs, list include: true rules for them followed by a catch-all { "pattern": "*", "include": false }. Because the first matching rule wins, the catch-all must come last. See Include only your application's URLs.
Still stuck?
- The UI Coverage FAQ answers focused questions about scores, views, grouping, identification, interaction commands, and profiles.
- Each configuration page has its own troubleshooting and "How rules are applied" section:
attributeFilters,elementFilters,elementGroups,elements,viewFilters,views, andprofiles. - The Configuration overview covers where configuration lives, how rules are applied, and how to regenerate reports.
- If a report still looks wrong after working through this page, reach out to your Cypress point of contact with a link to the run.