16

I am using Puppeteer for headless Chrome. I wish to evaluate a function inside the page that uses parts of other functions, defined dynamically elsewhere.

The code below is a minimal example / proof. In reality functionToInject() and otherFunctionToInject() are more complex and require the pages DOM.

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(someURL);       

var functionToInject = function(){
    return 1+1;
}

var otherFunctionToInject = function(input){
    return 6
}

var data = await page.evaluate(function(functionToInject, otherFunctionToInject){
    console.log('woo I run inside a browser')
    return functionToInject() + otherFunctionToInject();
});

return data

When I run the code, I get:

Error: Evaluation failed: TypeError: functionToInject is not a function

Which I understand: functionToInject isn't being passed into the page's JS context. But how do I pass it into the page's JS context?

mikemaccana
  • 110,530
  • 99
  • 389
  • 494
  • 1
    Does this answer your question? [How to pass a function in Puppeteers .evaluate() method?](https://stackoverflow.com/questions/47304665/how-to-pass-a-function-in-puppeteers-evaluate-method) – ggorlen May 02 '21 at 21:54

3 Answers3

31

You can add function to page context with addScriptTag:

const browser = await puppeteer.launch();
const page = await browser.newPage();

function functionToInject (){
    return 1+1;
}

function otherFunctionToInject(input){
    return 6
}

await page.addScriptTag({ content: `${functionToInject} ${otherFunctionToInject}`});

var data = await page.evaluate(function(){
    console.log('woo I run inside a browser')
    return functionToInject() + otherFunctionToInject();
});

console.log(data);

await browser.close();

This example is a dirty way of solving this problem with string concatenation. More clean would be using a url or path in the addScriptTag method.


Or use exposeFunction (but now functions are wrapped in Promise):

const browser = await puppeteer.launch();
const page = await browser.newPage();

var functionToInject = function(){
    return 1+1;
}

var otherFunctionToInject = function(input){
    return 6
}

await page.exposeFunction('functionToInject', functionToInject);
await page.exposeFunction('otherFunctionToInject', otherFunctionToInject);

var data = await page.evaluate(async function(){
    console.log('woo I run inside a browser')
    return await functionToInject() + await otherFunctionToInject();
});

console.log(data);

await browser.close();
mikemaccana
  • 110,530
  • 99
  • 389
  • 494
Everettss
  • 15,475
  • 9
  • 72
  • 98
  • 1
    I don't know - I never use it in that way. I've added another solution with `exposeFunction`. – Everettss Jan 11 '18 at 13:34
  • I know a thanks isn't necessary but just wanted to say it anyway - this is a really comprehensive answer with exampels and links to docs. – mikemaccana Jan 11 '18 at 13:34
  • Also `path` looks good for `addScriptTag` - no need for a custom URL schema to keep injected content seperate. – mikemaccana Jan 11 '18 at 13:35
  • 5
    It is good to know that with the first method (addScriptTag), the function executes within the browser. With the second method (exposeFunction) the function executes in Node.js. If you need to pass DOM elements to the function, the first method is the way to go. – barney765 Mar 03 '20 at 18:01
1

working example accessible by link, in the same repo you can see the tested component.

it("click should return option value", async () => {
    const optionToReturn = "ClickedOption";

    const page = await newE2EPage();
    const mockCallBack = jest.fn();

    await page.setContent(
      `<list-option option='${optionToReturn}'></list-option>`
    );

    await page.exposeFunction("functionToInject", mockCallBack); // Inject function
    await page.$eval("list-option", (elm: any) => {
      elm.onOptionSelected = this.functionToInject;  // Assign function
    });
    await page.waitForChanges();

    const element = await page.find("list-option");
    await element.click();
    expect(mockCallBack.mock.calls.length).toEqual(1); // Check calls
    expect(mockCallBack.mock.calls[0][0]).toBe(optionToReturn); // Check argument
  });
KEMBL
  • 536
  • 6
  • 10
0

You can also use page.exposeFunction() which will make your function return a Promise (requiring the use of async and await). This happens because your function will not be running inside your browser, but inside your nodejs application and its results are being send back and forth into/to the browser code.

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(someURL);       

var functionToInject = function(){
    return 1+1;
}

var otherFunctionToInject = function(input){
    return 6
}

await page.exposeFunction("functionToInject", functionToInject)
await page.exposeFunction("otherFunctionToInject", otherFunctionToInject)

var data = await page.evaluate(async function(){
    console.log('woo I run inside a browser')
    return await functionToInject() + await otherFunctionToInject();
});

return data

Related questions:

  1. exposeFunction() does not work after goto()
  2. exposed function queryseldtcor not working in 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. How to pass a function in Puppeteers .evaluate() method?
  10. Why can't I access 'window' in an exposeFunction() function with Puppeteer?
Evandro Coan
  • 8,560
  • 11
  • 83
  • 144