21

I have a very simple Puppeteer script that uses exposeFunction() to run something inside headless Chrome.

(async function(){

    var log = console.log.bind(console),
        puppeteer = require('puppeteer');


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

    var functionToInject = function(){
        return window.navigator.appName;
    }

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

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

    console.log(data);

    await browser.close();

})()

This fails with:

ReferenceError: window is not defined

Which refers to the injected function. How can I access window inside the headless Chrome?

I know I can do evaluate() instead, but this doesn't work with a function I pass dynamically:

(async function(){

    var log = console.log.bind(console),
        puppeteer = require('puppeteer');

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

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

    console.log(data);

    await browser.close();

})()
mikemaccana
  • 110,530
  • 99
  • 389
  • 494
  • I am not so sure why this is happening, but you can pass `window` as a variable and use it in your function. – fingeron Jan 16 '18 at 12:28
  • 2
    Are you sure? `Unhandled promise rejection (rejection id: 1): Error: Evaluation failed: TypeError: Converting circular structure to JSON` – mikemaccana Jan 16 '18 at 12:33
  • What do you mean by second script not working? Does it throw the same error as the first one? – Evandro Coan Apr 21 '20 at 06:17

3 Answers3

23

evaluate the function

You can pass the dynamic script using evaluate.

(async function(){
    var puppeteer = require('puppeteer');
    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    var functionToInject = function(){
        return window.navigator.appName;
    }

    var data = await page.evaluate(functionToInject); // <-- Just pass the function
    console.log(data); // outputs: Netscape
    
    await browser.close();
})()

addScriptTag and readFileSync

You can save the function to a seperate file and use the function using addScriptTag.

await page.addScriptTag({path: 'my-script.js'});

or evaluate with readFileSync.

await page.evaluate(fs.readFileSync(filePath, 'utf8'));

or, pass a parameterized funciton as a string to page.evaluate.

await page.evaluate(new Function('foo', 'console.log(foo);'), {foo: 'bar'});

Make a new function dynamically

How about making it into a runnable function :D ?

function runnable(fn) {
  return new Function("arguments", `return ${fn.toString()}(arguments)`);
}

The above will create a new function with provided arguments. We can pass any function we want.

Such as the following function with window, along with arguments,

function functionToInject() {
  return window.location.href;
};

works flawlessly with promises too,

function functionToInject() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(window.location.href);
    }, 5000);
  });
}

and with arguments,

async function functionToInject(someargs) {
  return someargs; // {bar: 'foo'}
};

Call the desired function with evaluate,

var data = await page.evaluate(runnable(functionToInject), {bar: "foo"});
console.log(data); // shows the location
floer32
  • 2,190
  • 4
  • 29
  • 50
Md. Abu Taher
  • 17,395
  • 5
  • 49
  • 73
  • This is best solution so far. the `await page.evaluate(fs.readFileSync(filePath, 'utf8'));` works at best allowing some structure in the code – albanx Mar 26 '22 at 21:05
15

exposeFunction() isn't the right tool for this job.

From the Puppeteer docs

page.exposeFunction(name, puppeteerFunction)

puppeteerFunction Callback function which will be called in Puppeteer's context.

'In puppeteer's context' is a little vague, but check out the docs for evaluate():

page.evaluateHandle(pageFunction, ...args)

pageFunction Function to be evaluated in the page context

exposeFunction() doesn't expose a function to run inside the page, but exposes a function to be be run in node to be called from the page.

I have to use evaluate():

mikemaccana
  • 110,530
  • 99
  • 389
  • 494
3

You problem could be related to the fact that page.exposeFunction() 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. This is why you function passed to page.exposeFunction() is now returning a promise instead of the actual result. And it explains why the window function is not defined, because your function is running inside nodejs (not your browser) and inside nodejs there is no window definition available.

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. How can I dynamically inject functions to evaluate using Puppeteer?
Evandro Coan
  • 8,560
  • 11
  • 83
  • 144