Skip to main content
UI CoveragePremium Solution

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.

Start here: regenerate the run after changing configuration

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 attributeFilters so UI Coverage falls back to a stable attribute. When you filter a dynamic id, also filter the attributes that reference it, such as for, aria-labelledby, and aria-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 elements rule 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-cy or data-test value. 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 their aria-label.

    App Quality Config
    {
    "significantAttributes": ["aria-label"]
    }

    Listing aria-label also 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 attributeFilters so 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 elementGroups rule 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-group attribute 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 over elementGroups configuration.

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 assert on a read-only badge or total, with allowedInteractionCommands.

    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, and attributeFilters, the first matching rule wins, so a broad rule above your specific one prevents it from applying. List specific rules first, and place include: true exceptions before the include: false rule they should override. (elements is 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 elementFilters or attributeFilters under a uiCoverage or accessibility key 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, and allowedInteractionCommands are UI Coverage–only and must live under the uiCoverage key, while views is set at the root. A misplaced property is rejected by the schema.
  • The pattern or selector doesn't match. attributeFilters patterns are anchored to the whole value, so value: "user" matches only the exact string user; use value: "user-.*" for a prefix. Selectors in elementFilters, elements, and elementGroups must match the interactive element itself, not a wrapper around it, so footer matches only the <footer>, while footer * 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?