Resource library

QA How-To

How to Fix Cypress "Element is being covered by another element"

Fix Cypress element is being covered by another element errors caused by overlays, loaders, and sticky headers with runnable tests and CI verification.

22 min read | 3,079 words

TL;DR

To fix Cypress Element Is Being Covered By Another Element, inspect the reported blocker, remove or wait out the overlay through the real UI, then click and assert the result. For a sticky header, try a centered scroll alignment; use force only when bypassing real interaction is explicitly the test goal.

Key Takeaways

  • Cypress checks the target center before an action and reports when another element covers it.
  • Dismiss consent banners and dialogs through their normal controls before acting on the page beneath them.
  • Wait for both the matching network response and the loading mask to clear.
  • Use center scroll alignment for a verified sticky-header overlap and repair layouts that stay obstructed.
  • A positional click is defensible only when its point is truly usable by a person.
  • Force clicks bypass actionability checks and can conceal a product defect.
  • Verify the original user outcome at the viewport and browser that produced the error.

If you need to fix Cypress Element Is Being Covered By Another Element, first identify what occupies the target's click point when Cypress acts. This error appears when a button or input exists but a modal, banner, loader, sticky header, or other element intercepts its center. Clear the obstruction as a user would, or correct the layout, then assert the intended action's result.

The element is being covered by another element

Cypress includes that wording under its "element cannot be interacted with" errors. The command log normally names the target and the element covering it; those details vary with your page. The Cypress error reference lists coverage separately from invisibility, a hidden center, and a disabled control. Preserve the full failure message and screenshot while diagnosing your own case.

TL;DR

If a cookie banner or dialog is in front of the target, dismiss it using its normal control and then click the target. If a transient loading layer covers the page, wait for the matching request and for the layer to disappear. If a fixed header traps the target after scrolling, try the documented scrollBehavior: 'center' click option and inspect the layout. Keep { force: true } for a deliberately non-user interaction, not as the default repair.

cy.get('[data-cy=cookie-banner] [data-cy=accept]').click()
cy.get('[data-cy=checkout]').click()
cy.get('[data-cy=checkout-status]').should('have.text', 'Checkout started')

These selectors refer to the runnable page below. The result assertion matters: merely seeing the button or passing .click() does not show that checkout started. Cypress's click guidance likewise recommends dismissing what covers a control before reaching for force.

What the Error Actually Means

Before an action command, Cypress scrolls the subject into view and checks whether it is hidden, disabled, detached, animating, or covered. For coverage it inspects the element's center. A child inside the button, such as an icon or label, is acceptable; a different element across that point is a blocker. Cypress retries actionability checks until the command times out. A green should('be.visible') does not settle the coverage question: current visibility behavior can regard a rendered control as visible even when an overlay covers its center. Read the Cypress actionability documentation for the exact distinction.

The blocker can be correct product behavior. An open modal should stop someone from clicking the page beneath it. A consent banner may demand a choice before checkout. In those cases, make the test perform the same decision as a person. A loading layer that remains after work finishes, or a header that permanently obscures a button, may instead be an application defect. A forced click can make a test pass while a real person remains stuck.

Cypress scrolls before clicking and may nudge the page to move a covered target away from a fixed header. If that process cannot free the center, inspect the geometry rather than assuming that an extra scrollIntoView() always helps. The scroll command yields its existing subject and is unsafe as a bridge to later subject-dependent actions; begin a new query before the click. The Cypress scrolling guide expands on scrollable containers and offsets.

Root-Cause Decision Table

Symptom Root cause Fix
Checkout button sits behind a consent panel The consent banner is still open Click its accept control, assert removal, then start checkout
Background control fails while a dialog is open Modal backdrop correctly blocks the page Close or complete the dialog before using the background
Click fails near a fixed header after scrolling The target's center lands under the header Use a center scroll alignment and repair obstructive layout
Button appears after data loads but a mask persists Loading state has not cleared or is stuck Wait for the request and assert the mask disappears
A floating help card covers part of a wide button Default center is obstructed while another point is usable Use a safe click position only when that is a real user path
Test passes only with force: true Actionability was bypassed Find the blocker and test the real interaction path

Start with the blocker reported by Cypress and the screenshot from the failing command. This table maps evidence to a specific remedy; applying every remedy in sequence only hides which behavior caused the failure. Compare the page in the runner with the same viewport used in CI because responsive overlays and sticky elements change position.

Reproducible Local Setup

Create a scratch directory, initialize npm, and install the Cypress version supported by your project. Do not pin a version copied from this article. Save the following page as site/covered-demo.html. It deliberately creates separate, visible blockers so every passing spec must remove a blocker or choose an unobstructed point. The demo does not depend on a framework, backend, or invented Cypress command.

npm init -y
npm install --save-dev cypress
mkdir -p site cypress/e2e
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Covered element demo</title>
<style>
  body { font: 16px system-ui; margin: 0; }
  [hidden] { display: none !important; }
  main { padding: 24px; }
  button { padding: 12px; cursor: pointer; }
  .banner { position: fixed; z-index: 30; inset: auto 0 0; height: 130px;
    background: #f5dd9b; display: grid; place-items: center; }
  .checkout-zone { position: fixed; bottom: 28px; left: 24px; z-index: 1; }
  .backdrop { position: fixed; z-index: 40; inset: 0;
    background: rgba(0,0,0,.5); display: grid; place-items: center; }
  .dialog { background: white; padding: 24px; }
  .sticky { position: sticky; top: 0; height: 96px; z-index: 20;
    background: #284b6f; color: white; padding: 8px; box-sizing: border-box; }
  .spacer { height: 700px; }
  .loading-area { position: relative; width: 320px; height: 100px; }
  .mask { position: absolute; z-index: 5; inset: 0;
    background: rgba(170,190,210,.85); display: grid; place-items: center; }
  .wide-area { position: relative; width: 360px; margin-top: 30px; }
  .wide-area button { width: 100%; }
  .help-card { position: absolute; z-index: 3; top: 0; left: 0;
    width: 220px; height: 46px; background: #b9d7f0; }
</style>
<header class="sticky">Demo header</header>
<main>
  <button data-cy="open-dialog">Open dialog</button>
  <button data-cy="background">Background action</button>
  <p data-cy="background-status"></p>
  <div class="spacer"></div>
  <button data-cy="low-action">Action below header</button>
  <p data-cy="low-status"></p>
  <button data-cy="load">Load account</button>
  <div class="loading-area">
    <button data-cy="account-action">Account action</button>
    <div data-cy="loading-mask" class="mask" hidden>Loading</div>
  </div>
  <p data-cy="account-status"></p>
  <div class="wide-area">
    <button data-cy="wide-action">Wide action</button>
    <div data-cy="help-card" class="help-card">Help</div>
  </div>
  <p data-cy="wide-status"></p>
</main>
<div class="checkout-zone">
  <button data-cy="checkout">Checkout</button>
  <p data-cy="checkout-status"></p>
</div>
<div data-cy="cookie-banner" class="banner">
  <button data-cy="accept">Accept cookies</button>
</div>
<div data-cy="backdrop" class="backdrop" hidden>
  <div class="dialog" role="dialog" aria-modal="true">
    <button data-cy="close-dialog">Close dialog</button>
  </div>
</div>
<script>
  const one = selector => document.querySelector(selector)
  one('[data-cy=accept]').onclick = () => one('[data-cy=cookie-banner]').remove()
  one('[data-cy=checkout]').onclick = () =>
    one('[data-cy=checkout-status]').textContent = 'Checkout started'
  one('[data-cy=open-dialog]').onclick = () =>
    one('[data-cy=backdrop]').hidden = false
  one('[data-cy=close-dialog]').onclick = () =>
    one('[data-cy=backdrop]').hidden = true
  one('[data-cy=background]').onclick = () =>
    one('[data-cy=background-status]').textContent = 'Background used'
  one('[data-cy=low-action]').onclick = () =>
    one('[data-cy=low-status]').textContent = 'Low action used'
  one('[data-cy=load]').onclick = async () => {
    one('[data-cy=loading-mask]').hidden = false
    const response = await fetch('/api/account')
    if (!response.ok) throw new Error('Account request failed')
    await response.json()
    one('[data-cy=loading-mask]').hidden = true
  }
  one('[data-cy=account-action]').onclick = () =>
    one('[data-cy=account-status]').textContent = 'Account action used'
  one('[data-cy=wide-action]').onclick = () =>
    one('[data-cy=wide-status]').textContent = 'Wide action used'
</script>
</html>

Save this config as cypress.config.js. Python's HTTP server serves the page, while cy.intercept() supplies the one demo API response. Run the server in one terminal and the spec in another. Add each it(...) block from the numbered sections to cypress/e2e/covered.cy.js; top-level Cypress tests are valid, and each test visits a fresh page.

const { defineConfig } = require('cypress')
module.exports = defineConfig({
  e2e: { baseUrl: 'http://127.0.0.1:4173' },
})
python3 -m http.server 4173 --directory site

Before writing a spec, check http://127.0.0.1:4173/covered-demo.html in a browser. You should see a consent bar across the bottom and a dark blue header. The server command stays active; use a second terminal for each verification command below. If your shell cannot find python3, use any static server that keeps this URL and port consistent with the config.

1. Fix Cypress Element Is Being Covered By Another Element Under a Consent Banner

The demo's checkout button is fixed near the bottom, while the consent bar covers that region. Clicking checkout first should fail because the consent bar lies over its center. The banner is part of the intended flow, so the test should exercise it rather than modify CSS or bypass the actionability check. A real application might store consent and remove the banner on the next visit; this scratch page resets on every load so the behavior is deterministic.

it('dismisses consent before checkout', () => {
  cy.visit('/covered-demo.html')
  cy.get('[data-cy=cookie-banner] [data-cy=accept]').click()
  cy.get('[data-cy=cookie-banner]').should('not.exist')
  cy.get('[data-cy=checkout]').click()
  cy.get('[data-cy=checkout-status]').should('have.text', 'Checkout started')
})

Run npx cypress run --spec cypress/e2e/covered.cy.js. The spec should exit successfully and its checkout assertion should pass. If your product has a different consent path, use its actual accept, reject, or preferences control; do not hard-code consent into localStorage merely to silence this failure. Testing through the visible choice also catches a banner that refuses to close. For suites that do not cover consent in every test, isolate consent setup in a deliberate helper and retain a dedicated consent-flow test. The Cypress data-cy selector guide shows how to keep these controls identifiable without relying on styling classes.

2. Close a Modal That Covers the Background Control

An open dialog brings its backdrop above the rest of the page. If the test attempts to click a background action while the dialog is open, Cypress is reporting a real interaction restriction. The remedy is to complete or dismiss the dialog, then act on the page. In the demo, opening and closing happen through ordinary buttons, and the final assertion proves that the background action ran only after the backdrop stopped intercepting it.

it('closes the dialog before using the background', () => {
  cy.visit('/covered-demo.html')
  cy.get('[data-cy=open-dialog]').click()
  cy.get('[data-cy=backdrop]').should('be.visible')
  cy.get('[data-cy=close-dialog]').click()
  cy.get('[data-cy=backdrop]').should('not.be.visible')
  cy.get('[data-cy=background]').click()
  cy.get('[data-cy=background-status]').should('have.text', 'Background used')
})

Run npx cypress run --spec cypress/e2e/covered.cy.js and confirm this test is green. If the real dialog must be submitted rather than closed, assert its confirmation and that the backdrop has gone before moving on. A backdrop that stays in the DOM with display: none is fine; one with transparent styling and active pointer interception is not. Inspect the element named in the Cypress error and the computed pointer-events value when the visible dialog seems gone but the click still fails. Repair the lingering backdrop in application code, because it can trap users too.

3. Fix Cypress Element Is Being Covered By Another Element Near a Sticky Header

Cypress normally scrolls a target into view before clicking. It may also nudge the page if a fixed or sticky header covers the target. Some layouts still produce an overlap, especially inside nested scrolling containers or with large fixed navigation. In that case, changing the action's alignment to the center is more precise than an arbitrary cy.scrollTo() distance. The demo places the low action after a tall spacer so you can check that the documented option is accepted and the click reaches the right control.

it('centers a low action before clicking', () => {
  cy.visit('/covered-demo.html')
  cy.get('[data-cy=low-action]').click({ scrollBehavior: 'center' })
  cy.get('[data-cy=low-status]').should('have.text', 'Low action used')
})

Run npx cypress run --spec cypress/e2e/covered.cy.js. The low-action status should change. This example verifies center alignment, but the basic demo may also pass with default scrolling because Cypress's automatic nudge is designed for sticky headers. Apply the option to a real failure only after inspecting the screenshot and scroll container. If the target cannot be exposed at any scroll position, fix the product's layout or provide enough scroll space. You can set scrollBehavior per action, which avoids changing unrelated interactions. Do not treat .should('be.visible') as proof that the center is clear; the action checks coverage separately.

4. Wait Until a Loading Mask Actually Clears

The load button puts a mask over the account action while it fetches /api/account. The mask is an intentional temporary block. Register the intercept before clicking Load, wait for its alias, and then assert that the mask is hidden. The UI assertion is still necessary: a successful HTTP response does not itself prove that the application removed the overlay. The demo's response is a small stub, and the application deliberately waits for JSON parsing before hiding the mask.

it('waits for the account mask to clear', () => {
  cy.intercept('GET', '/api/account', { statusCode: 200, body: { id: 42 } })
    .as('account')
  cy.visit('/covered-demo.html')
  cy.get('[data-cy=load]').click()
  cy.wait('@account').its('response.statusCode').should('eq', 200)
  cy.get('[data-cy=loading-mask]').should('not.be.visible')
  cy.get('[data-cy=account-action]').click()
  cy.get('[data-cy=account-status]').should('have.text', 'Account action used')
})

Run npx cypress run --spec cypress/e2e/covered.cy.js and look for the account status assertion. The exact response fields in your product should match its real contract; a stub with the wrong shape can make a test pass for the wrong reason. If the mask never goes away after a successful response, debug the application's state transition instead of adding cy.wait(2000). If a request never begins, inspect the trigger and intercept pattern rather than extending the command timeout. The Cypress intercept guide and alias wait guide explain how to match and await the correct request.

5. Use an Unobstructed Click Position Only When It Matches User Behavior

Cypress clicks the center by default. A partially covered, wide control may still have an exposed region a person can use. The demo's help card occupies the left side of the wide button, including its center; its right side is free. Choose a point such as bottomRight only after confirming that the point lies inside the visible button, does not hit the help card, and represents the interaction you actually intend to test. This is a narrow workaround for a partial overlap, not a repair for a fully covered control.

it('uses the exposed side of a wide action', () => {
  cy.visit('/covered-demo.html')
  cy.get('[data-cy=wide-action]').click('bottomRight')
  cy.get('[data-cy=wide-status]').should('have.text', 'Wide action used')
})

Run npx cypress run --spec cypress/e2e/covered.cy.js. The status proves the button received a normal Cypress click at the requested position. If your test relies on such a point, add a separate layout assertion or visual review for the partial overlap. A help card covering half a primary action may be a product defect even though the test can reach its edge. Prefer moving the card, reducing its hit area, or changing the UI layout when the overlap is accidental. Cypress also accepts relative click coordinates, but hard-coded pixels can break with font and viewport changes.

CI and Docker Variants

CI can expose a blocker that is absent locally because viewport dimensions, font loading, or response timing differ. Run the focused spec at the failing viewport and browser, then inspect the failed screenshot and command log. In the scratch example, this shell sequence starts the static server, waits for the page, runs the spec, and stops the server on exit. Use it from the scratch directory after saving the page, config, and tests.

python3 -m http.server 4173 --directory site &
server_pid=$!
trap 'kill "$server_pid"' EXIT
until curl -fsS http://127.0.0.1:4173/covered-demo.html >/dev/null; do sleep 1; done
npx cypress run --spec cypress/e2e/covered.cy.js --config viewportWidth=1280,viewportHeight=800

The command's zero exit status verifies the tests at that viewport. Use the actual CI dimensions if they differ. A failure isolated to a narrow view often points to a responsive menu, cookie panel, or floating widget covering a control. The Cypress flaky-test guide helps separate timing from viewport and state variation; retries alone cannot make a blocked button usable.

For Docker, select a published cypress/included image matching your installed Cypress version. Start the Python server on the host first, then run the container from the scratch directory. The placeholder is intentional; obtain the actual tag from the official image listing rather than guessing. The host mapping below is for Docker environments that support host-gateway; otherwise put the server and Cypress in the same container network and use its reachable hostname.

npx cypress --version
docker run --rm --add-host=host.docker.internal:host-gateway \
  -v "$PWD:/work" -w /work \
  cypress/included:<your-cypress-version> \
  --config baseUrl=http://host.docker.internal:4173,viewportWidth=1280,viewportHeight=800 \
  --spec cypress/e2e/covered.cy.js

The included image starts Cypress itself, so the arguments after its tag are runner options. Confirm that the host server is reachable from the container before interpreting a navigation failure as a coverage error. Keep the viewport and browser settings aligned with the environment where the issue appeared. For an application behind authentication, reproduce the same signed-in state and feature flags before comparing screenshots.

How to Verify the Fix

Run the original failing test with only the cause-specific change. Confirm the failed click now executes without force and that the application shows the expected result: checkout starts, a dialog action completes, or the account state updates. Next, run the focused suite at the CI viewport and browser. If a screenshot was captured on failure, compare it with the repaired page at the same point in the flow.

To diagnose a stubborn overlap, pause before the action in the Cypress runner and inspect the page. In the browser console, document.elementFromPoint(x, y) can show what occupies a viewport coordinate. Compute the target's center from getBoundingClientRect() and compare that hit test with the element Cypress reports. This is diagnostic, not a replacement for a user-facing assertion: the point can change when the page scrolls or an overlay animates.

cy.get('[data-cy=checkout]').then(($button) => {
  const rect = $button[0].getBoundingClientRect()
  const x = rect.left + rect.width / 2
  const y = rect.top + rect.height / 2
  cy.log(`Center hit: ${$button[0].ownerDocument.elementFromPoint(x, y)?.outerHTML}`)
})

Use that snippet before the click if you need evidence; it does not wait for an overlay to disappear. Run npx cypress run --spec cypress/e2e/covered.cy.js after removing any temporary diagnostic commands. If the error returns only in a real application, inspect the blocker at failure time, the request timeline, and whether the viewport differs. A passing isolated demo confirms the Cypress technique; the production path still needs its own outcome assertion. The single-test Cypress guide can help narrow a large suite during investigation.

Prevent It From Coming Back

Give overlays explicit lifecycle rules. A loader should appear when work begins and disappear on success and failure. A dialog backdrop should close with its dialog. A dismissed consent panel should not leave a transparent hit box. Product tests should verify these transitions, including error paths where a request rejects. If the page uses a third-party widget, agree on where it can cover primary actions at each breakpoint.

Keep Cypress selectors tied to intent, such as data-cy=checkout, and scope repeated controls to their panel. Assert that a blocker is absent or hidden before the next action when the journey requires its removal. Use aliases for relevant requests, then inspect rendered state. Avoid fixed sleeps: they provide no evidence about whether the mask or banner actually cleared. Preserve CI screenshots and videos for actionability failures so the reported covering element can be matched to the page.

Review forced clicks. A test that routinely uses { force: true } on visible user controls may be missing an interaction step or concealing a layout defect. Cypress documents that force skips scrolling and several checks, including coverage. It can dispatch an event to a control that a person cannot reach. If direct event dispatch is genuinely the purpose of a lower-level test, state that purpose in the test name and keep a separate end-to-end path with ordinary clicks. The modern Cypress test architecture guide helps decide which behavior belongs in each layer.

Interview Questions and Answers

Q: What does the covered-element error tell you? Cypress found the target but another element occupied the point where the action would occur. I would inspect the named blocker and the screenshot before changing the test.

Q: Does should('be.visible') prove a button can be clicked? No. Visibility and center-point coverage are distinct checks. The action command performs its own actionability checks before firing events.

Q: When is a modal-related failure expected? An open modal should block background controls. I would complete or dismiss the modal through its UI, assert that the backdrop cleared, and only then click the background.

Q: How would you handle a loader covering a button? I would register an intercept before the triggering action, await the matching request, and assert that the loader disappeared. The rendered-state check catches a UI that remains blocked after the response.

Q: Why might scrollBehavior: 'center' help? It changes where Cypress positions the target before the click. For a sticky header overlap, centering can put the target's click point away from the header, though the layout should still be reviewed.

Q: Is { force: true } a good general fix? No. It skips coverage and other actionability checks, so a test can pass when a user cannot interact. I would first identify why the target is blocked and test the intended path.

Q: When would a positional click be reasonable? A wide control may have an exposed region that users can actually select. I would verify the chosen position and assert the result, while also flagging accidental overlap for product repair.

Common Mistakes

  • Adding .should('be.visible') and assuming that it waits out every overlay. Cypress can consider a covered element visible.
  • Calling click({ force: true }) on a checkout or submit button whose center is blocked. This hides the actual user problem.
  • Waiting a fixed number of milliseconds for a loader. Wait for the relevant request and the loader's removal.
  • Registering cy.intercept() after clicking the control that starts the request. The request may already be gone.
  • Clicking a background button through an open dialog. Close or finish the dialog first.
  • Adding scrollIntoView() without checking which element is at the center after scrolling. Cypress already scrolls before action commands.
  • Clicking a free corner of a button without checking whether the obstruction is a layout bug.
  • Reproducing only at a desktop viewport when the CI run uses a smaller one.

Conclusion

To fix Cypress Element Is Being Covered By Another Element, determine which element blocks the target's action point and why it is there. Dismiss expected overlays, wait for transient masks to clear, adjust scroll alignment when a header interferes, and repair layouts that leave controls obstructed. Run the original path without force and assert the user-visible result at the viewport where the failure occurred.

Interview Questions and Answers

How does Cypress decide that an element is covered?

Cypress evaluates actionability before clicking, including whether another element occupies the target center. A child inside the target is acceptable; an unrelated overlay can block the action. I would inspect the covering element named in the failure and the screenshot.

How would you troubleshoot a covered checkout button?

I would inspect the failed screenshot and identify whether a consent panel, modal, or loading mask covers checkout. Then I would follow the actual user path that clears it and assert the checkout outcome. I would keep force out of the end-to-end path.

What is the difference between a coverage failure and a detached element?

A covered target is still in the document, but another element intercepts its action point. A detached subject was removed from the document after selection. Coverage calls for resolving the blocker; detachment usually calls for a fresh query after the render transition.

What should be asserted after waiting for a network alias?

I would check the relevant response status or body and then assert the UI state that makes the action possible. The response can complete while a loading mask remains mounted. The final business result also needs its own assertion.

Why might a sticky header cause a Cypress click failure?

The target can align beneath fixed or sticky navigation after scrolling. Cypress normally nudges it clear, but complex scroll containers can still overlap. I would inspect the geometry, try a center scroll alignment, and fix the layout if the control remains unreachable.

What are the trade-offs of click with force true?

Force sends events while bypassing actionability checks, including coverage. It can be useful for a deliberate low-level event test, but it weakens a user-journey test. I would document its purpose and maintain an ordinary-click path for the user behavior.

How would you handle a modal backdrop left behind after closing a dialog?

I would first assert the backdrop becomes hidden or absent when the dialog closes. If it remains transparent but intercepts clicks, that is a product defect, not a Cypress timing issue. I would repair the dialog lifecycle and add a regression test.

What evidence distinguishes a timing issue from a layout defect?

I would compare the reported blocker, screenshots, and page state across passing and failing runs at the same viewport. A mask that disappears after a request suggests a transition boundary; a fixed widget that always overlaps a button suggests layout. I would verify the chosen remedy by asserting the actual action result.

Frequently Asked Questions

What does element is being covered by another element mean in Cypress?

The intended target exists, but a different element occupies its action point. Cypress refuses the click because a normal user interaction would likely hit the blocker. Inspect the reported covering element and its lifecycle.

Why can a visible Cypress element still be covered?

Visibility and coverage are separate conditions. A rendered button may satisfy a visibility assertion while a banner or backdrop occupies its center. The click command checks coverage before firing events.

Should I use click with force true to fix a covered element?

Usually no. Force bypasses the coverage check and can make a test pass while the control remains unusable. Dismiss the blocker or repair the UI, then assert the action result.

How do I fix a cookie banner covering a Cypress button?

Use the banner's visible accept, reject, or preferences control. Assert that the banner is removed or hidden before clicking the underlying button. Keep a separate test for the consent choice itself.

What if a loading overlay covers the button after an API call?

Register a matching intercept before the request starts, wait for its alias, and assert that the overlay clears. A completed response alone does not prove the application updated its UI.

Can Cypress scrollBehavior fix a sticky header overlap?

The documented center alignment can move a target away from a sticky header before clicking. Cypress also nudges automatically, so inspect the actual failure before changing the option. Fix the layout if no scroll position exposes the control.

When is clicking at bottomRight acceptable?

A positional click can be appropriate when that point is visibly exposed and represents a real user path. Assert the resulting action and review whether the partial overlap is itself a defect.

Why does this fail only in CI?

CI may use a different viewport, browser, or response timing that moves or prolongs the blocker. Compare screenshots and command logs at the same dimensions, then verify the fix in that environment.

Related Guides