Puppeteer:如何等待元素可见?

93

我想知道是否可以告诉 Puppeteer 等待元素显示。

const inputValidate = await page.$('input[value=validate]');
await inputValidate.click()
        
// I want to do something like that 
waitElemenentVisble('.btnNext ')

const btnNext = await page.$('.btnNext');
await btnNext.click();

有没有任何方法可以实现这个?


1
关于模态框的注意事项(虽然这不是被问到的,但我认为这是一个常见的陷阱):使用淡入/淡出效果的模态框时,元素的可见性很棘手。一个元素可能是可见的,但由于模态框的不透明度等原因,它还不能被点击。你可以禁用过渡效果进行测试,或者只需注册显示/隐藏钩子,在窗口上写一个布尔变量,并在模态交互测试中等待正确的值。这样可以避免许多错误。 - ron
8个回答

115

我认为你可以使用 page.waitForSelector(selector[, options]) 函数来实现这个目的。

const puppeteer = require('puppeteer');

puppeteer.launch().then(async browser => {

   const browser = await puppeteer.launch({executablePath: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", headless: false});
   const page = await browser.newPage();
   await page.setUserAgent(options.agent);
   await page.goto("https://www.url.net", {timeout: 60000, waitUntil: 'domcontentloaded'});
   
   page
    .waitForSelector('#myId')
    .then(() => console.log('got it'));
    browser.close();
});

要检查可用选项,请参见GitHub链接。


2
waitForXPath() ? - Gilles Quénot
1
@joy,你可以直接使用page.waitFor,它非常灵活。 - JamieJag
19
这是一个好答案吗?代码片段甚至没有访问网页! - shafeen
4
@shafeen,我给这个片段提出了修改建议,但它还没有被接受:/ 不过,我认为这个片段仍然很重要,因为它演示了如何使用page.waitForSelector - Nino Filiu
1
示例代码未经测试且缺少URL。 - Gabriel Rodriguez
显示剩余4条评论

81

如果你想确保该元素实际上是可见的,你必须使用

await page.waitForSelector('#myId', {visible: true})
否则,您只是在DOM中查找元素而不检查其可见性。

5
在我的情况下,我需要相反的操作,即 page.waitForSelector('#myId', {hidden: true}) ,在继续之前等待加载器隐藏。 - Chris Magnuson
如果元素的高度/宽度为0,则此方法无效。请查看以下答案以了解更多信息:https://dev59.com/3FYO5IYBdhLWcg3wPvJv#54103671 - Aalex Gabi

50

注意,直到今天为止提交的所有答案都是错误的。

因为它回答了一个元素是否存在或位于,但不是可见或显示

正确的答案是使用page.waitFor()page.waitForFunction()检查元素的大小或可见性,详情见下面的解释。

// wait until present on the DOM
// await page.waitForSelector( css_selector );
// wait until "display"-ed
await page.waitForFunction("document.querySelector('.btnNext') && document.querySelector('.btnNext').clientHeight != 0");
// or wait until "visibility" not hidden
await page.waitForFunction("document.querySelector('.btnNext') && document.querySelector('.btnNext').style.visibility != 'hidden'");

const btnNext = await page.$('.btnNext');
await btnNext.click();

说明

如果在页面的DOM中存在元素,但是该元素具有CSS属性display:nonevisibility:hidden,那么它不一定可见,因此使用page.waitForSelector(selector)并不是一个好主意。让我们看看以下代码片段中的差异。

function isExist(selector) {
  let el = document.querySelector(selector);
  let exist = el.length != 0 ? 'Exist!' : 'Not Exist!';
  console.log(selector + ' is ' + exist)
}

function isVisible(selector) {
  let el = document.querySelector(selector).clientHeight;
  let visible = el != 0 ? 'Visible, ' + el : 'Not Visible, ' + el;
  console.log(selector + ' is ' + visible + 'px')
}

isExist('#idA');
isVisible('#idA');
console.log('=============================')
isExist('#idB')
isVisible('#idB')
.bd {border: solid 2px blue;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="bd">
  <div id="idA" style="display:none">#idA, hidden element</div>
</div>
<br>
<div class="bd">
  <div id="idB">#idB, visible element</div>
</div>

在上面的片段中,函数isExist()被模拟。
page.waitForSelector('#myId');

我们可以看到在运行isExist()检查#idA#idB两个元素时都返回存在。

但是在运行isVisible()检查时,#idA不可见或未显示。

此外还有其他的对象可以用来检查元素是否被显示,例如使用CSS属性display

scrollWidth
scrollHeight
offsetTop
offsetWidth
offsetHeight
offsetLeft
clientWidth
clientHeight

为了检查样式 visibility,请使用非 hidden

注意:我不擅长Javascript或英语,请随意改进此答案。


3
完全正确!然而,Puppeteer实际上有一个选项可以做到这一点。请参见我的答案。 - finn
1
@finn,除非下一页具有完全相同的选择器,否则您无法区分当前页面和下一页。waitForFunction可以让您做到这一点。 - caram
根据发布说明,waitFor()已在15版本中被移除。 - Stan Smith

18
您可以使用 page.waitFor(), page.waitForSelector(), 或者 page.waitForXPath() 来等待页面上的元素:page
// Selectors

const css_selector = '.btnNext';
const xpath_selector = '//*[contains(concat(" ", normalize-space(@class), " "), " btnNext ")]';

// Wait for CSS Selector

await page.waitFor(css_selector);
await page.waitForSelector(css_selector);

// Wait for XPath Selector

await page.waitFor(xpath_selector);
await page.waitForXPath(xpath_selector);

注意:在引用frame时,你也可以使用frame.waitFor(), frame.waitForSelector(), 或者frame.waitForXPath()

1
等待 page.waitFor 不是多余的吗? - FabricioG
3
是的,它现在已经被弃用了。 - Moshisho

13

通过一些优化,更新了答案:

const puppeteer = require('puppeteer');

(async() => {
    const browser = await puppeteer.launch({headless: true});
    const page = await browser.newPage();

    await page.goto('https://www.somedomain.com', {waitUntil: 'networkidle2'});
    await page.click('input[value=validate]');
    await page.waitForSelector('#myId');
    await page.click('.btnNext');
    console.log('got it');

    browser.close();
})();

7
虽然我同意 @ewwink 的答案。Puppeteer的API默认检查未隐藏,因此当您执行以下操作时:
await page.waitForSelector('#id', {visible: true})

您无法通过CSS获得隐藏和可见的元素。 为确保渲染,您可以像@ewwink的waitForFunction一样操作。但是,为了完全回答您的问题,这里是一个使用puppeteer API的代码片段:
async waitElemenentVisble(selector) {
  function waitVisible(selector) {
    function hasVisibleBoundingBox(element) {
      const rect = element.getBoundingClientRect()
      return !!(rect.top || rect.bottom || rect.width || rect.height)
    }
    const elements = [document.querySelectorAll(selector)].filter(hasVisibleBoundingBox)
    return elements[0]
  }
  await page.waitForFunction(waitVisible, {visible: true}, selector)
  const jsHandle = await page.evaluateHandle(waitVisible, selector)
  return jsHandle.asElement()
}

在我自己编写了一些类似的方法之后,我发现 expect-puppeteer 可以更好地执行这个任务(请查看 toMatchElement)。


3
async function waitForVisible (selector){
    //const selector = '.foo';
  return  await page.waitForFunction(
      (selector) => document.querySelector(selector) && document.querySelector(selector).clientHeight != 0",
      {},
      selector
    );
}

上述功能是通用的,因此您可以在任何地方使用它。


但是,如果您正在使用pptr,则有另一种更快速、更简单的解决方案:

https://pptr.dev/#?product=Puppeteer&version=v10.0.0&show=api-pagewaitforfunctionpagefunction-options-args


page.waitForSelector('#myId', {visible: true})

2

我刚刚通过抓取一个健身网站测试了这个。@ewwink、@0fnt和@caram提供了最完整的答案。

仅仅因为一个DOM元素可见,并不意味着它的内容已经完全填充。

今天,我运行了:

await page.waitForSelector("table#some-table", {visible:true})
const data = await page.$eval("table#some-table",(el)=>el.outerHTML)
console.log(data)

因为表格DOM还没有完全由运行时填充,所以收到了以下错误。您可以看到outerHTML为空。

user@env:$ <table id="some-table"></table>

添加1秒的暂停后,问题得到了解决,这是可以预料的。

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

await page.waitForSelector("table#some-table", {visible:true})
await sleep(1000)
const data = await page.$eval("table#some-table",(el)=>el.outerHTML)
console.log(data)

user@env:$ <table id="some-table"><tr><td>数据</td></tr></table>

但是,@ewwink的答案更加优雅(没有人为超时):

await page.waitForSelector("table#some-table", {visible:true})
await page.waitForFunction("document.querySelector('table#sched-records').clientHeight != 0")
const data = await page.$eval("table#some-table",(el)=>el.outerHTML)
console.log(data)

user@env:$ <table id="some-table"><tr><td>数据</td></tr></table>


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