如何使用Puppeteer和纯JavaScript检查元素是否可见?

26
我希望用纯JavaScript(而不是jQuery)和Puppeteer来检查DOM元素是否可见,该怎么做?在这里“可见”指的是该元素通过CSS呈现,而不是隐藏的(例如通过display: none)。例如,我可以通过以下方式确定我的元素#menu是否未被CSS规则display: none隐藏:
const isNotHidden = await page.$eval('#menu', (elem) => {
  return elem.style.display !== 'none'
})

我怎样一般性地确定元素是否隐藏,而不只是通过 display: none 属性?


1
我不知道对于其他的隐藏方法它是否能够一直正常工作,但是elem.getBoundingClientRect()返回了可以进行测试的独特数据。 - AuxTaco
@AuxTaco 使用elem.getBoundingClientRect()在控制台上返回一个{},无论元素是否准备好:( - PayamB.
由于Puppeteer始终可以运行正常的JS,因此应链接规范线程检查DOM中元素是否可见,因为它具有大量资源,可以通过.evaluate()在Puppeteer中使用。 - ggorlen
@PayamB. 你试过返回 JSON.stringify(elem.getBoundingClientRect()) 吗?这很重要的原因是 elem.getBounding... 是一个只读的 DOMRect 而不是一个普通对象,所以 Puppeteer 的序列化似乎受到影响,无法捕获完整的对象。 - ggorlen
13个回答

50
我发现 Puppeteer 有一个 API 方法可以实现这个目的:Page.waitForSelector,通过它的 visible 选项。我之前不知道后者选项,但它可以让你等到元素可见为止。
await page.waitForSelector('#element', {
  visible: true,
})

相反,您可以通过 hidden 选项等待元素被隐藏。

我认为这是关于 Puppeteer API 的惯用答案。感谢 Colin Cline,因为我认为他的答案可能对一般的 JavaScript 解决方案有用。


4
如果元素不可见,这将抛出异常。如果元素不可见是完全可以接受的,那么抛出异常就不合适了。我希望开发人员能停止因为正常行为而抛出异常,而是返回一个像null这样的值! - Gerry
1
@Gerry,这正是我的问题,我想检查元素,如果该元素没有准备好,那么我想基于此做一些操作,但是当寻找该元素时它会抛出异常并导致应用崩溃。 - PayamB.
1
问题是如何测试一个元素是否可见,而不是等待它被添加到DOM中并变得可见,这就是这个“被接受”的解决方案所做的。 - user3483642
这对于位于其他元素z-Index后面的元素不起作用。 - lowcrawler
1
我不明白可接受的答案是什么意思,如果元素由于等待而不存在,它将使脚本暂停。 - Wazime

15

第一步是通过检查其显示样式值来确定元素是否可见。 第二步是通过检查其高度来确定元素是否可见。例如,如果该元素是 display: none 父元素的子元素,则 offsetHeight 将为 0,因此您知道该元素虽然具有显示值,但不可见。我们不会检查 opacity: 0 元素是否隐藏。

const isNotHidden = await page.$eval('#menu', (elem) => {
    return window.getComputedStyle(elem).getPropertyValue('display') !== 'none' && elem.offsetHeight
});

在进行任何计算之前,您可以检查 elem.offsetWidth,这也不错,可以检查元素是否存在。


“window”变量在哪里定义的? - pabgaran
1
@pabgaran window对象表示包含DOM文档的窗口; getComputedStyle。你可以将窗口缓存到变量中,但它是一个全局对象。 - Colin Cline
你是对的。我是一个puppetter的初学者。所有这些代码都是在“导航器”内运行的,所以窗口、文档等始终可用。 - pabgaran
这个解决方案刚好救了我的一天,除了我检查了元素的高度。 - PayamB.

10

问题中的链接指向 Github,但 Github 返回 404。我提交了一个指向 pptr 文档网站上相同内容的链接。https://pptr.dev/api/puppeteer.elementhandle.boundingbox/ - Lucas

8

当前被接受的答案涉及等待元素出现并变得可见。

如果我们不想等待元素,并且只想测试元素的可见性,我们可以使用getComputedStyle()getBoundingClientRect()的组合来检测元素是否可见。

我们首先可以检查visibility是否未设置为hidden

然后,我们可以验证边界框是否可见,通过检查bottomtopheightwidth属性是否未设置为0(这将过滤掉同时具有display设置为none的元素)。

const element_is_visible = await page.evaluate(() => {
  const element = document.querySelector('#example');
  const style = getComputedStyle(element);
  const rect = element.getBoundingClientRect();

  return style.visibility !== 'hidden' && !!(rect.bottom || rect.top || rect.height || rect.width);
});

1
这对于那些在其他元素后面的元素(即:z-index中堆叠的元素)无效...是否有像这样的全功能解决方案呢? - lowcrawler

5

也许你可以使用elementHandle.boundingBox()(感谢@huypham的想法)

它将返回一个Promise,显示元素相对于主框架的边界框,如果元素不可见,则返回null。

示例代码片段:

      const loadMoreButton = await getDataPage.$(
        'button.ao-tour-reviews__load-more-cta.js-ao-tour-reviews__load-more-cta'
      );

      const buttonVisible = await loadMoreButton.boundingBox();

      if (buttonVisible) {
        await loadMoreButton.click().catch((e) => {
          console.log(': ' + e)
        });
      }

2

@aknuds1 给出的答案非常完美,但您可以通过创建此类助手使其更加方便。如果元素可见,则解决为true,否则为false

function isElementVisible(page, selector, timeout = 150) {
    return new Promise((resolve) => {
        page.waitForSelector(selector, {visible: true, timeout}).then(() => {
            resolve(true);
        }).catch(() => {
            resolve(false);
        });
    });
}

使用方法

使用纯JSPuppeteer

let isVisible = await isElementVisible(page, selector)
isVisible = await isElementVisible(page, selector, 300)

如果您使用Jest或其他框架


expect(await isElementVisible(page, selector)).toBeTrue();

对于Jest(以及大多数其他框架)而言,您甚至可以更进一步,创建自定义匹配器来扩展现有的匹配器。(https://jestjs.io/docs/expect#expectextendmatchers

expect.extend({
    async toHaveVisible(page, selector, timeout = 150) {
        let isVisible = await isElementVisible(page, selector, timeout);

        if (isVisible) {
            return {
                message: () => `expected ${selector} not to be visible`,
                pass: true
            };
        } else {
            return {
                message: () => `expected ${selector} to be visible`,
                pass: false
            };
        }
    }
});

await expect(page).toHaveVisible(selector);
await expect(page).not.toHaveVisible(anotherSelector);
await expect(page).not.toHaveVisible(yetAnotherSelector, 300);

2

基于剧作家的逻辑来检查元素是否可见 - https://github.com/microsoft/playwright/blob/master/src/server/injected/injectedScript.ts#L120-L129

function isVisible(element: Element): boolean {
    // Note: this logic should be similar to waitForDisplayedAtStablePosition() to avoid surprises.
    if (!element.ownerDocument || !element.ownerDocument.defaultView)
      return true;
    const style = element.ownerDocument.defaultView.getComputedStyle(element);
    if (!style || style.visibility === 'hidden')
      return false;
    const rect = element.getBoundingClientRect();
    return rect.width > 0 && rect.height > 0;
  }

0

显然,这是jQuery的做法:

visible = await page.evaluate((e) => e.offsetWidth > 0 && e.offsetHeight > 0, element)

0
您还可以尝试使用Element.checkVisibility()方法。它在Chrome中受支持。以下是它的工作原理,来自CSS规范

调用此元素的checkVisibility(options)方法必须执行以下步骤:

  1. 如果此元素没有关联的框,则返回false。
  2. 如果此元素的包括阴影的祖先元素具有content-visibility: hidden样式,则返回false。
  3. 如果options的checkOpacity字典成员为true,并且此元素或其包括阴影的祖先元素的计算透明度值为0,则返回false。
  4. 如果options的checkVisibilityCSS字典成员为true,并且此元素不可见,则返回false。
  5. 返回true。

0

这段代码肯定会对你有所帮助。它基本上意味着该元素已经在页面上可用,但尚未可见或在CSS中,显示属性设置为none或可见性被隐藏。现在,在编写我们的测试时,我们假设一旦该元素可用,就对其执行操作,如单击或键入。但是,由于此元素尚未可见,Puppeteer无法执行该操作。

async function isLocatorReady(element, page) {
  const isVisibleHandle = await page.evaluateHandle((e) => 
{
    const style = window.getComputedStyle(e);
    return (style && style.display !== 'none' && 
    style.visibility !== 'hidden' && style.opacity !== '0');
 }, element);
  var visible = await isVisibleHandle.jsonValue();
  const box = await element.boxModel();
  if (visible && box) {
    return true;
  }
  return false;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接