Playwright - how to check if element is in viewport?
Asked Answered
M

8

6

I have an array-like object of nodes (it is a carousel), their order is randomly generated with each page refresh, playwright finds all the elements to be visible, but some of them are outside the viewport (based on the error received). I need to make sure, that element is inside the viewport when attempting to click it, otherwise I get an error stating the element is outside.

How to determine if a randomly picked node element of an array-like object is actually within the viewport?

Midrash answered 4/3, 2021 at 14:52 Comment(0)
T
5

Playwright unfortunately doesn't have a method like isInterSectingViewport in Puppeteer yet.(likethis)

So authors of Playwright help me in Slack community(you can find it on official site).

    const result = await page.$eval(selector, async element => {
      const visibleRatio: number = await new Promise(resolve => {
        const observer = new IntersectionObserver(entries => {
          resolve(entries[0].intersectionRatio);
          observer.disconnect();
        });
        observer.observe(element);
        // Firefox doesn't call IntersectionObserver callback unless
        // there are rafs.
        requestAnimationFrame(() => {});
      });
      return visibleRatio > 0;
    });

The case in which I used this method: I want know that after I’m click some element - I have scrolling to another element. Unfortunately boundingBox method doesn’t help in my case.

also You can add this functionality to my BasePage class

/**
     * @returns {!Promise<boolean>}
     */
  isIntersectingViewport(selector: string): Promise<boolean> {
    return this.page.$eval(selector, async element => {
      const visibleRatio: number = await new Promise(resolve => {
        const observer = new IntersectionObserver(entries => {
          resolve(entries[0].intersectionRatio);
          observer.disconnect();
        });
        observer.observe(element);
        // Firefox doesn't call IntersectionObserver callback unless
        // there are rafs.
        requestAnimationFrame(() => {});
      });
      return visibleRatio > 0;
    });
  }

P.S. In fact, all the code except for one line is taken from the realisation of method isInterSectingViewport in GitHub Puppeteer

Transpolar answered 19/8, 2021 at 12:56 Comment(0)
D
5

Playwright has added a feature to enable checking this using "expect". The usage info is below:

 (method) LocatorAssertions.toBeInViewport(options?: {
    ratio?: number;
    timeout?: number;
}): Promise<void>
Ensures the Locator points to an element that intersects viewport, according to the intersection observer API.

Usage

const locator = page.getByRole('button');
// Make sure at least some part of element intersects viewport.
await expect(locator).toBeInViewport();
// Make sure element is fully outside of viewport.
await expect(locator).not.toBeInViewport();
// Make sure that at least half of the element intersects viewport.
await expect(locator).toBeInViewport({ ratio: 0.5 });
Danille answered 11/10, 2023 at 6:19 Comment(0)
B
3

To check if element is in viewport using css selector:

import { test, expect, devices } from '@playwright/test'

const url = 'https://example.com'
const selector = 'h1'

test.use({
    headless: false,
    browserName: 'webkit',
    ...devices['iPhone 13 Mini'],
})

test('visibility', async ({ page }) => {
    await page.goto(url)
    const box = await page.locator(selector).boundingBox() // it contains x, y, width, and height only
    let isVisible = await page.evaluate((selector) => {
        let isVisible = false
        let element = document.querySelector(selector)
        if (element) {
            let rect = element.getBoundingClientRect()
            if (rect.top >= 0 && rect.left >= 0) {
                const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
                const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
                if (rect.right <= vw && rect.bottom <= vh) {
                    isVisible = true
                }
            }
        }
        return isVisible
    }, selector)
    await expect(isVisible).toBeTruthy()
})
Bouncy answered 30/11, 2021 at 19:17 Comment(0)
G
2

If you need to check if element is in viewport outside assertion here is helpful function:

    const isInViewport = async (element: Locator): Promise<boolean> => {
      const viewportSize = element.page().viewportSize();
      const boundingBox = await element.boundingBox();
    
      if ( !viewportSize || !boundingBox) {
        return false;
      }
    
      const isBoundingBoxVisible = boundingBox.x >= 0 && boundingBox.y >= 0;
      const isBoundingBoxInViewport =
        boundingBox.x + boundingBox.width <= viewportSize.width &&
        boundingBox.y + boundingBox.height <= viewportSize.height;
    
      return isBoundingBoxVisible && isBoundingBoxInViewport;
    };
Genteel answered 10/12, 2023 at 6:56 Comment(0)
P
1

Everything inside evaluate(callback) is hard to debug. Because the callback function is running inside the browser scope.

The other way is to utilize locator.boundingBox() and page.viewportSize() to implement your own expect(locator).toBeInViewport(), which sadly, only return void officially. Meaning we cannot receive a boolean, and perform subsequent actions based on that value. (e.g. do a scroll to emulate human behaviour)

Implementation is pretty simple:

const box = await mylocator.boundingBox()
const view = page.viewportSize() // note: doesn't need await
const isMyLocatorInViewport = box.y + box.height - view.height < 0 && box.y > 0

Only work on Y axis but enough for a lot of scenarios.

Photograph answered 26/11, 2023 at 19:51 Comment(0)
M
0
const firstId = "#someId"; 

// it happens that I am evaluating in a frame, rather than page 
const result = await frame.evaluate((firstId) => {

  // define a function that handles the issue
  // returns true if element is within viewport, false otherwise 
  function isInViewport(el) {

    // find element on page 
    const element = document.querySelector(el); 

    const rect = element.getBoundingClientRect();
    return (
      rect.top >= 0 &&
      rect.left >= 0 &&
      rect.bottom <=
        (window.innerHeight || document.documentElement.clientHeight) &&
      rect.right <=
        (window.innerWidth || document.documentElement.clientWidth)
    );
  }; 

  return isInViewport(firstId); 

}, firstId);



// back to node context 
console.log(result); 
Midrash answered 4/3, 2021 at 15:36 Comment(0)
O
0

in addition to cardinalX answer. You can create helper function using page.waitForFunction to wait until element to be in viewport

import { Page } from '@playwright/test';

export const waitToBeInViewport = async (
  page: Page,
  selector: string,
) => page.waitForFunction(async (selectorParam: string) => {
  const element = document.querySelector(selectorParam);
  const visibleRatio: number = await new Promise((resolve) => {
    const observer = new IntersectionObserver((entries) => {
      resolve(entries[0].intersectionRatio);
      observer.disconnect();
    });
    observer.observe(element);
    requestAnimationFrame(() => { });
  });
  return visibleRatio > 0; // where 0 - element has just appeared, and 1 - element fully visible;
}, selector);
Outpour answered 6/8, 2022 at 14:57 Comment(1)
the return value im getting from this function is {"_guid": "handle@73d2b54286357906ed761446c1a5c78a", "_type": "JSHandle"}. probably im not using it correctly. any tips?Carcassonne
C
0

In addition to TeaDrinkers answer, this one worked for me:

async function isInterSectingViewport(locator: Locator, page: Page): Promise<boolean> {
    const elementsBoundingBox = await locator.boundingBox();
    const viewportSize = page.viewportSize();
    if (elementsBoundingBox == null || viewportSize == null) {
        return false;
    }
    return elementsBoundingBox.x < viewportSize.width
        && elementsBoundingBox.x + elementsBoundingBox.width > 0
        && elementsBoundingBox.y < viewportSize.height
        && elementsBoundingBox.y + elementsBoundingBox.height > 0;
}
Condemnation answered 4/7 at 7:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.