Search code examples
javascriptnode.jsgoogle-chrome-devtoolspuppeteerdevtools

What happens when we call puppeteer waitForSelector API


This question is based on Puppeteer and headless Chrome interaction (based on chrome devtools protocol).

Puppeteer sends JSON formatted messages to Chrome devtools to control chrome operations like accessing a page, typing in a textfield or click a button etc.

When we execute below line (this helps to wait till #username is visible)

await page.waitForSelector('#username', { visible: true });

Puppeteer sends below 5 messages to Chrome.

{"sessionId":"EB950D87CE0E2EED6D432F080811B87D","method":"Runtime.callFunctionOn","params":{"functionDeclaration":"async function waitForPredicatePageFunction(predicateBody, polling, timeout, ...args) {\n  const predicate = new Function('...args', predicateBody);\n  let timedOut = false;\n  if (timeout)\n    setTimeout(() => timedOut = true, timeout);\n  if (polling === 'raf')\n    return await pollRaf();\n  if (polling === 'mutation')\n    return await pollMutation();\n  if (typeof polling === 'number')\n    return await pollInterval(polling);\n\n  /**\n   * @return {!Promise<*>}\n   */\n  function pollMutation() {\n    const success = predicate.apply(null, args);\n    if (success)\n      return Promise.resolve(success);\n\n    let fulfill;\n    const result = new Promise(x => fulfill = x);\n    const observer = new MutationObserver(mutations => {\n      if (timedOut) {\n        observer.disconnect();\n        fulfill();\n      }\n      const success = predicate.apply(null, args);\n      if (success) {\n        observer.disconnect();\n        fulfill(success);\n      }\n    });\n    observer.observe(document, {\n      childList: true,\n      subtree: true,\n      attributes: true\n    });\n    return result;\n  }\n\n  /**\n   * @return {!Promise<*>}\n   */\n  function pollRaf() {\n    let fulfill;\n    const result = new Promise(x => fulfill = x);\n    onRaf();\n    return result;\n\n    function onRaf() {\n      if (timedOut) {\n        fulfill();\n        return;\n      }\n      const success = predicate.apply(null, args);\n      if (success)\n        fulfill(success);\n      else\n        requestAnimationFrame(onRaf);\n    }\n  }\n\n  /**\n   * @param {number} pollInterval\n   * @return {!Promise<*>}\n   */\n  function pollInterval(pollInterval) {\n    let fulfill;\n    const result = new Promise(x => fulfill = x);\n    onTimeout();\n    return result;\n\n    function onTimeout() {\n      if (timedOut) {\n        fulfill();\n        return;\n      }\n      const success = predicate.apply(null, args);\n      if (success)\n        fulfill(success);\n      else\n        setTimeout(onTimeout, pollInterval);\n    }\n  }\n}\n//# sourceURL=__puppeteer_evaluation_script__\n","executionContextId":4,"arguments":[{"value":"return (function predicate(selectorOrXPath, isXPath, waitForVisible, waitForHidden) {\n      const node = isXPath\n        ? document.evaluate(selectorOrXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue\n        : document.querySelector(selectorOrXPath);\n      if (!node)\n        return waitForHidden;\n      if (!waitForVisible && !waitForHidden)\n        return node;\n      const element = /** @type {Element} */ (node.nodeType === Node.TEXT_NODE ? node.parentElement : node);\n\n      const style = window.getComputedStyle(element);\n      const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();\n      const success = (waitForVisible === isVisible || waitForHidden === !isVisible);\n      return success ? node : null;\n\n      /**\n       * @return {boolean}\n       */\n      function hasVisibleBoundingBox() {\n        const rect = element.getBoundingClientRect();\n        return !!(rect.top || rect.bottom || rect.width || rect.height);\n      }\n    })(...args)"},{"value":"raf"},{"value":30000},{"value":"#username"},{"value":false},{"value":true},{"value":false}],"returnByValue":false,"awaitPromise":true,"userGesture":true},"id":28}

---------------

{"sessionId":"EB950D87CE0E2EED6D432F080811B87D","method":"Runtime.callFunctionOn","params":{"functionDeclaration":"s => !s\n//# sourceURL=__puppeteer_evaluation_script__\n","executionContextId":4,"arguments":[{"objectId":"{\"injectedScriptId\":4,\"id\":3}"}],"returnByValue":true,"awaitPromise":true,"userGesture":true},"id":29}

-------------

{"sessionId":"EB950D87CE0E2EED6D432F080811B87D","method":"DOM.describeNode","params":{"objectId":"{\"injectedScriptId\":4,\"id\":3}"},"id":30}

-------------

{"sessionId":"EB950D87CE0E2EED6D432F080811B87D","method":"DOM.resolveNode","params":{"backendNodeId":11,"executionContextId":3},"id":31}

-------------

{"sessionId":"EB950D87CE0E2EED6D432F080811B87D","method":"Runtime.releaseObject","params":{"objectId":"{\"injectedScriptId\":4,\"id\":3}"},"id":32}

I am trying to understand whats happening here. The 1st message looks a javascript function. Is this Javascript function gets executed at hesdless chrome.

Basically I need to understand clearly whats happening when waitForSelector is executed.

EDIT The javascript function if I extract from first JSON messages looks as below.

async function waitForPredicatePageFunction(predicateBody, polling, timeout, ...args) {\
  const predicate = new Function('...args', predicateBody);\
  let timedOut = false;\
  if (timeout)\
    setTimeout(() => timedOut = true, timeout);\
  if (polling === 'raf')\
    return await pollRaf();\
  if (polling === 'mutation')\
    return await pollMutation();\
  if (typeof polling === 'number')\
    return await pollInterval(polling);\
\
  /**\
   * @return {!Promise<*>}\
   */\
  function pollMutation() {\
    const success = predicate.apply(null, args);\
    if (success)\
      return Promise.resolve(success);\
\
    let fulfill;\
    const result = new Promise(x => fulfill = x);\
    const observer = new MutationObserver(mutations => {\
      if (timedOut) {\
        observer.disconnect();\
        fulfill();\
      }\
      const success = predicate.apply(null, args);\
      if (success) {\
        observer.disconnect();\
        fulfill(success);\
      }\
    });\
    observer.observe(document, {\
      childList: true,\
      subtree: true,\
      attributes: true\
    });\
    return result;\
  }\
\
  /**\
   * @return {!Promise<*>}\
   */\
  function pollRaf() {\
    let fulfill;\
    const result = new Promise(x => fulfill = x);\
    onRaf();\
    return result;\
\
    function onRaf() {\
      if (timedOut) {\
        fulfill();\
        return;\
      }\
      const success = predicate.apply(null, args);\
      if (success)\
        fulfill(success);\
      else\
        requestAnimationFrame(onRaf);\
    }\
  }\
\
  /**\
   * @param {number} pollInterval\
   * @return {!Promise<*>}\
   */\
  function pollInterval(pollInterval) {\
    let fulfill;\
    const result = new Promise(x => fulfill = x);\
    onTimeout();\
    return result;\
\
    function onTimeout() {\
      if (timedOut) {\
        fulfill();\
        return;\
      }\
      const success = predicate.apply(null, args);\
      if (success)\
        fulfill(success);\
      else\
        setTimeout(onTimeout, pollInterval);\
    }\
  }\
}\
//# sourceURL=__puppeteer_evaluation_script__\

And in argument list I see below arguments

return (function predicate(selectorOrXPath, isXPath, waitForVisible, waitForHidden) {\
      const node = isXPath\
        ? document.evaluate(selectorOrXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue\
        : document.querySelector(selectorOrXPath);\
      if (!node)\
        return waitForHidden;\
      if (!waitForVisible && !waitForHidden)\
        return node;\
      const element = /** @type {Element} */ (node.nodeType === Node.TEXT_NODE ? node.parentElement : node);\
\
      const style = window.getComputedStyle(element);\
      const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();\
      const success = (waitForVisible === isVisible || waitForHidden === !isVisible);\
      return success ? node : null;\
\
      /**\
       * @return {boolean}\
       */\
      function hasVisibleBoundingBox() {\
        const rect = element.getBoundingClientRect();\
        return !!(rect.top || rect.bottom || rect.width || rect.height);\
      }\
    })(...args)

Then other arguments I see

raf

3000

username

false

true

false

These are all information. I am not able to relate altogether what is happening. Can you please explain in detail whats going on here.


Solution

  • waitFors in Puppeteer are being solved using polling. waitForSelector uses the raf option for polling:

    raf: constantly execute pageFunction in requestAnimationFrame callback. This is the tightest polling mode which is suitable to observe styling changes.

    So, basically. waitForSelector will send a function that will run on each requestAnimationFrame. That function will resolve a promise when the selector is visible (or hidden, depending on your options), or when it timeouts.

    When that function is serialized and sent to Chromium this is what would happen:

    • waitForPredicatePageFunction will be executed.
    • As the polling method will be raf, it will await pollRaf.
    • pollRaf will execute the function it got as an argument, in this case, a selector check.
    • If false will return a promise.
    • It will call itself calling requestAnimationFrame.
    • It will loop until the predicate returns true or it timeouts.