microsoft / microsoft/playwright

[Feature]: Extend isVisible/toBeVisible API to include overlap detection

Open
#34,778 11 comments 10 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

P3-collecting-feedback
Dominant language
TypeScript
Stars
96.3k
Forks
6.5k
Avg merge
1d 6h
Merged PRs (30d)
180

Description

🚀 Feature Request

Extend the isVisible and toBeVisible calls to have an optional flag checkOverlaps (or enforceNotOverlapped if we want to be more precise), which would check if the element is overlapped by another one and thus not visible. For backward-compatibility, the flag would have a default value of false, in which case isVisible/toBeVisible would work exactly the same way as it does now.

If there is a strong argument for keeping these existing APIs unchanged, a new method could also be introduced (e.g. isNotOverlapped/toBeNotOverlapped).

Here's a possible implementation (does not consider transformations yet) that we use in our codebase to check overlaps.


function isElementNotOverlapped (element: Locator): Promise<boolean> {
  return element.evaluate((el) => {
    // pointer-events: none interferes with elementFromPoint so we temporarily set the property to all
    const originalPointerEvents = el.style.pointerEvents

    el.style.pointerEvents = 'all'
    const rect = el.getBoundingClientRect()

    const getStyleValueAsNumber = (styleProperty: string) => {
      return Number(window.getComputedStyle(el, null).getPropertyValue(styleProperty)
        .replace('px', ''))
    }

    const paddingLeft = getStyleValueAsNumber('padding-left')
    const paddingRight = getStyleValueAsNumber('padding-right')
    const paddingTop = getStyleValueAsNumber('padding-top')
    const paddingBottom = getStyleValueAsNumber('padding-bottom')
    const borderRadiusTopLeft = getStyleValueAsNumber('border-top-left-radius')
    const borderRadiusTopRight = getStyleValueAsNumber('border-top-right-radius')
    const borderRadiusBottomLeft = getStyleValueAsNumber('border-bottom-left-radius')
    const borderRadiusBottomRight = getStyleValueAsNumber('border-bottom-right-radius')

    const isPointVisible = (x: number, y: number) => {
      const elementAtPoint = document.elementFromPoint(x, y)

      return el.contains(elementAtPoint) || elementAtPoint === el
    }

    const pointsToCheckOffset = 2
    const leftEdgeToCheck = rect.left + paddingLeft + pointsToCheckOffset
    const topEdgeToCheck = rect.top + paddingTop + pointsToCheckOffset
    const rightEdgeToCheck = rect.right - paddingRight - pointsToCheckOffset
    const bottomEdgeToCheck = rect.bottom - paddingBottom - pointsToCheckOffset

    const pointsToCheck = [
      // top-left corner
      {x: leftEdgeToCheck + (borderRadiusTopLeft / 3), y: topEdgeToCheck + (borderRadiusTopLeft / 3)},
      // top-right corner
      {x: rightEdgeToCheck - (borderRadiusTopRight / 3), y: topEdgeToCheck + (borderRadiusTopRight / 3)},
      // bottom-left corner
      {x: leftEdgeToCheck + (borderRadiusBottomLeft / 3), y: bottomEdgeToCheck - (borderRadiusBottomLeft / 3)},
      // bottom-right corner
      {x: rightEdgeToCheck - (borderRadiusBottomRight / 3), y: bottomEdgeToCheck - (borderRadiusBottomRight / 3)},
      // center
      {x: rect.left + (rect.width / 2), y: rect.top + (rect.height / 2)}
    ]

    const result = pointsToCheck.every((point) => isPointVisible(point.x, point.y))

    el.style.pointerEvents = originalPointerEvents

    return result
  })
Example
await page.goto('data:text/html,<html><body style="margin: 0;">' +
    '<div id="small-box" style="background-color: green; height: 50px; width: 50px; position: fixed; top: 80px">' +
    '</div><div id="big-box" style="background-color: brown; height: 100px; width: 100px; position: fixed;">' +
    '</div></html>')

const smallBox = page.locator('#small-box')

// Passes
await expect(smallBox).toBeVisible()
// Passes
await expect(smallBox).toBeInViewport()
  
// Does not pass, since overlapped by `#big-box`.
await expect(smallBox).toBeVisible({checkOverlaps: true})
Motivation

As described in this previous issue (as well as this one), isVisible currently does not consider overlaps at all, so if an element is fully behind another one, isVisible may still return true for it. It would be useful to be able to test that an element is actually visible on the screen, since some visual bugs can offer due to overlapping elements and these are currently hard to write tests for using Playwright. Modern web apps often feature a lot of popups, modals etc. that may overlap other content on the screen.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing the isVisible and toBeVisible APIs and reviewing the linked issues 9923 and 15924. Compare the proposed optional overlap flag with the alternative API, then validate the example with overlapping fixed-position elements; done means the chosen API consistently distinguishes ordinary visibility from overlap detection without changing the default behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
playwright, typescript
Domain
testing
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.