41

Whenever I try to pass a function, like this:

var myFunc = function() { console.log("lol"); };

await page.evaluate(func => {
 func();
 return true;
}, myFunc);

I get:

(node:13108) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Evaluation failed: TypeError: func is not a function
at func (<anonymous>:9:9)
(node:13108) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Why? How to do it correctly?

Thank you!

€: let me clarify: I am doing it this way because I want to find some DOM elements first and use them inside of that function, more like this (simplified):

var myFunc = function(element) { element.innerHTML = "baz" };

await page.evaluate(func => {
  var foo = document.querySelector('.bar');
  func(foo);
  return true;
}, myFunc);
chitzui
  • 3,311
  • 4
  • 22
  • 38
  • 2
    Does this answer your question? [How can I dynamically inject functions to evaluate using Puppeteer?](https://stackoverflow.com/questions/48207414/how-can-i-dynamically-inject-functions-to-evaluate-using-puppeteer) – user Apr 21 '20 at 01:58

7 Answers7

47

You cannot pass a function directly into page.evaluate(), but you can call another special method (page.exposeFunction), which expose your function as a global function (also available in as an attribute of your page window object), so you can call it when you are inside page.evaluate():

var myFunc = function() { console.log("lol"); };
await page.exposeFunction("myFunc", myFunc);

await page.evaluate(async () => {
   await myFunc();
   return true;
});

Just remember that page.exposeFunction() will make your function return a Promise, then, you need to use async and await. This happens because your function will not be running inside your browser, but inside your nodejs application.

  1. exposeFunction() does not work after goto()
  2. Why can't I access 'window' in an exposeFunction() function with Puppeteer?
  3. How to use evaluateOnNewDocument and exposeFunction?
  4. exposeFunction remains in memory?
  5. Puppeteer: pass variable in .evaluate()
  6. Puppeteer evaluate function
  7. allow to pass a parameterized funciton as a string to page.evaluate
  8. Functions bound with page.exposeFunction() produce unhandled promise rejections
  9. exposed function queryseldtcor not working in puppeteer
  10. How can I dynamically inject functions to evaluate using Puppeteer?
user
  • 7,316
  • 9
  • 70
  • 122
25

Similar problems have been discussed in a puppeteer issue.

There are several way to deal with your problem. First rule is to keep it simple.

Evaluate the function

This is the fastest way to do things, you can just pass the function and execute it.

await page.evaluate(() => {
  var myFunc = function(element) { element.innerHTML = "baz" };
  var foo = document.querySelector('.bar');
  myFunc(foo);
  return true;
});

Expose the function beforehand

You can expose the function beforehand using a page.evaluate, or a page.addScriptTag

// add it manually and expose to window
await page.evaluate(() => {
  window.myFunc = function(element) { element.innerHTML = "baz" };
});

// add some scripts
await page.addScriptTag({path: "myFunc.js"});

// Now I can evaluate as many times as I want
await page.evaluate(() => {
  var foo = document.querySelector('.bar');
  myFunc(foo);
  return true;
});

Use ElementHandle

page.$(selector)

You can pass an element handle to .evaluate and make changes as you seem fit.

const bodyHandle = await page.$('body');
const html = await page.evaluate(body => body.innerHTML, bodyHandle);

page.$eval

You can target one element and make changes as you want.

const html = await page.$eval('.awesomeSelector', e => {
e.outerHTML = "whatever"
});

The trick is to read the docs and keep it simple.

ealfonso
  • 5,901
  • 4
  • 35
  • 63
Md. Abu Taher
  • 15,529
  • 5
  • 45
  • 64
  • 1
    What's the point of first solution, *you can just pass the function and execute it.*? What if we want to use the same `myFunc` in multiple `evaluate` calls? – avocado Jul 18 '20 at 15:40
  • 1
    like the answer below says, you can't pass a function into the page like you can a variable using `page.evaluate`. ex: `await page.evaluate(async func =>{ /* ... */ }, myFunc)` – Leviathan_the_Great Nov 19 '20 at 22:35
4

Pass function with parameter

// add it manually and expose to window

 await page.evaluate(() => {
      window.myFunc = function(element) { element.innerHTML = "baz" };
    });

// and then call function declared above

 await page.evaluate((param) => {
         myFunc (param);
    }, param);
vnguyen
  • 127
  • 5
  • I am not supposed to call `window.myFunc` instead of `myFunc` inside `page.evaluate(...)`? How `myFunc` ended up in the global namespace after being assigned to `window.myFunc`? – user Apr 21 '20 at 01:27
  • 2
    How you answer is different from the most voted answer? – user Apr 21 '20 at 01:29
0
//  External function to run inside evaluate context
function getData() {
        return document.querySelector('title').textContent;
    }

function mainFunction(url, extractFunction){
    let browser = await puppeteer.launch({});
    let page = await browser.newPage();

    await page.goto(url);

    let externalFunction = Object.assign(extractFunction);

    let res = await this.page.evaluate(externalFunction)

    console.log(res);
}
    

// call it here
mainFunction('www.google.com',getData);
sea
  • 181
  • 2
  • 9
-1

The error is thrown because you execute func(); but func is not a function. I update my answer to answer your updated question:

Option 1: execute your function in page context:

var myFunc = function(element) { element.innerHTML = "baz" };
await page.evaluate(func => {
  var foo = document.querySelector('.bar');
  myFunc(foo);
  return true;
});

Option 2: pass element handle as arguments

const myFunc = (element) => { 
    innerHTML = "baz";
    return true;
}
const barHandle = await page.$('.bar');
const result = await page.evaluate(myFunc, barHandle);
await barHandle.dispose();

`

Giang Nguyen
  • 7
  • 1
  • 3
  • 2
    nah not really, I want to evaluate myFunc inside the .evaluate function. In such a way that I can do stuff inside the browser context before and use that to call myFunc also inside of the browser context: – chitzui Nov 15 '17 at 10:26
  • Being able to do other stuff before and pass it to the function: await page.evaluate(func => { /* do stuff here */ func( /* use the stuff in here */ ); return true; }, myFunc); – chitzui Nov 15 '17 at 10:27
  • So you can do like this: ` var myFunc = function(element) { element.innerHTML = "baz" }; await page.evaluate(() => { var foo = document.querySelector('.bar'); myFunc(foo); return true; }); ` – Giang Nguyen Nov 15 '17 at 10:56
  • This is wrong. You cannot pass global scope functions from your tests into the page.evaluate as you are doing. – user Apr 21 '20 at 01:26
-1

Created a helper function that wraps page.evaluate:

const evaluate = (page, ...params) => browserFn => {
    const fnIndexes = [];
    params = params.map((param, i) => {
        if (typeof param === "function") {
            fnIndexes.push(i);
            return param.toString();
        }
        return param;
    });
    return page.evaluate(
        (fnIndexes, browserFnStr, ...params) => {
            for (let i = 0; i < fnIndexes.length; i++) {
                params[fnIndexes[i]] = new Function(
                    " return (" + params[fnIndexes[i]] + ").apply(null, arguments)"
                );
            }
            browserFn = new Function(
                " return (" + browserFnStr + ").apply(null, arguments)"
            );
            return browserFn(...params);
        },
        fnIndexes,
        browserFn.toString(),
        ...params
    );
};

export default evaluate;

Takes all parameters and converts functions to string.
Then recreates functions in browser context.
See https://github.com/puppeteer/puppeteer/issues/1474

You can use this function like so:

const featuredItems = await evaluate(page, _getTile, selector)((get, s) => {
    const items = Array.from(document.querySelectorAll(s));
    return items.map(node => get(node));
});
  • 1
    What you are doing here with all this code? How to use it? – user Apr 21 '20 at 01:45
  • This lets me create modularised functions for the browser context, import them into my scripts and pass them to evaluates. ie. _getTile can access the document, be imported from its own Javascript file, then be passed to the evaluate method. By stringifying a function then recreating in the browser context, you can pass a function to a Puppeteer .evaluate() method, as mentioned in the question. – Ryan Soury Jun 17 '20 at 12:39
-1
function stringifyWithFunc(obj) {
  const str = JSON.stringify(obj, function(key, val) {
      if (typeof val === "function") {
        return val + "";
        return val;
      });
    return str;
  }

  function parseWithFunction(str) {
    const obj = JSON.parse(str, function(key, val) {
      if (typeof val === 'string' && val.includes("function")) {
        return eval(`(${val})`);
      }
      return val;
    });
    return obj;
  }

  function testFunc() {
    console.log(123);
  };

  const params = {
    testFunc,
    a: 1,
    b: null
  }

  await page.exposeFunction("parseWithFunction", parseWithFunction);

  await pageFrame.$eval(".category-content", (elem, objStr) => {
      const params = parseWithFunction(objStr);
      params.testFunc()
    },
    stringifyWithFunc(params)
  );
Дмытрык
  • 243
  • 1
  • 12