I want to run a function inside evaluate(), I'm passing it as an argument, but I'm getting 'func is not a function', what am I missing?
Puppeteer version: 10.2
Platform / OS version: Windows 10, Node 8.2.1
var func = function() {
console.log("xxxxx");
};
var response = await page.evaluate( (func) => {
func(); //func is not a function
}, func);
You can use page.exposeFunction(name, puppeteerFunction):
await page.exposeFunction("add", (a, b) => a + b);
const response = await page.evaluate(() => {
return window.add(2, 2);
});
If I understand it correctly, puppeteer has to marshal the code in your evaluate function ultimately as a string and inject it into to page context. You can't pass function references or anything non-serializable across that boundary.
Related
I am playing around with some office JavaScript and attempting to create an executable function from a string that is received from an API call.
The office JavaScript task pane for Excel makes a call to an external API on button click, and returns a function in the form of a String object. To make this into a function object, I have used:
var executable = new Function(response)
executable();
Unfortunately, nothing is happening, it doesn't seem to be calling the function at all.
After some debugging, I believe the reason it isn't getting called is because the response string object is already a full function, and new Function() is wrapping the response in another layer of function.
Response is:
async function highlightCells() {
await Excel.run(async (context) => {
const sheet = context.workbook.worksheets.getItem("Sheet1");
const range = sheet.getRange();
range.format.fill.color = "yellow";
await context.sync();
console.log("Called");
});
}
And executable is resolving to:
function anonymous() {
async function highlightCells() {
await Excel.run(async (context) => {
const sheet = context.workbook.worksheets.getItem("Sheet1");
const range = sheet.getRange();
range.format.fill.color = "yellow";
await context.sync();
console.log("Called");
});
}
}
Any ideas how to prevent the additional function wrapper from appearing? As you can see the response object is already a full function.
Do I need to use some other method of converting the string to a function or is there a way to override the wrapper within the new Function() syntax?
If you don't know the function name in advance, you can wrap the function definition in brackets to call it.
let response = `async function test() {
console.log("function called");
}`;
let executable = new Function(`(${response})();`);
executable();
If you need to pass it arguments or await it, make it return the function and call the function to get your actual function.
let func = `async function sum(a,b) {
return new Promise(resolve => setTimeout(() => resolve(a+b), 1000));
}`;
let executable = new Function(`return ${func};`)();
(async () => {
let val = await executable(3,4);
console.log("the sum is", val);
})();
If you know that it is guaranteed to be a function you could directly invoke it in the Function:
let data = 'function(arg1, arg2) { return arg1 + " " + arg2 }'
let func = new Function(`return (${data}).apply(this, arguments)`)
console.log(func(1,2))
With .apply(this, arguments) you call that function and pass the arguments you pass to your Function object into the received function. And the return returns the result that function.
Passing this to apply ensures that the Function object could be stored in an object and that the function your received could access that object using this. This might not be required, but makes the function behave like a regular function:
let data = 'function(arg1, arg2) { return arg1 + " " + arg2 + " " + this.prop }'
let obj = {prop : 'somevalue'}
obj.func = new Function(`return (${data}).apply(this, arguments)`)
console.log(obj.func(1, 2))
Simply use eval instead of new Function. You need to force the code to be an expression, not a statement, that's what the 0, part is for.
code = `function test() {
console.log('hey!')
}`
let executable = eval('0,' + code)
executable()
var executable = new Function(response + ';highlightCells()')
executable();
Since the returned code is a complete executable statement, use eval() to execute it, not new Function().
eval(response);
highlightCells();
Note that this requires that you know the name of the function that's being defined in the response. If not, you need to write code that parses it to extract the function name.
The below script works, but I don't understand why one doesn't have to
const a = promisify(opA());
instead of the (correct)
const a = promisify(opA);
I mean opA is a function, so why not opA()?
'use strict'
const { promisify } = require('util')
const opA = (cb) => {
setTimeout(() => {
cb(null, 'A')
}, 500)
}
const opB = (cb) => {
setTimeout(() => {
cb(null, 'B')
}, 250)
}
const opC = (cb) => {
setTimeout(() => {
cb(null, 'C')
}, 125)
}
const a = promisify(opA);
const b = promisify(opB);
const c = promisify(opC);
(async function() {
try {
console.log(await a());
console.log(await b());
console.log(await c());
} catch(err) {
print(err);
};
})();
I mean opA is a function, so why not opA()?
Because opA is a reference to the function itself. The promise will use that reference to execute that function at a later time.
Alternatively, opA() executes the function (without any arguments) now and passes the result of that function call to the promise. Since your opA function doesn't return anything, it would pass undefined to the promise. The promise would then have nothing to execute later after it completes its operation(s). The setTimeout would then also fail because cb is also undefined, because no arguments were passed to opA().
Any time you do see a structure like that, either (1) it's a bug or (2) the function intentionally builds and returns a callback function intended for the promise. For example, if you write a function which returns opA then you can invoke that function and pass its result to promisify.
An important clue to this behavior is here:
const opA = ...
opA is a variable, not unlike any other variable. It contains a value or a reference to something. In this case it's a reference to a function. You could re-assign that variable, pass it as a function argument, set it as a property on an object, etc. just like any other.
I am using page.$$eval to assess div#Menu in the browser context. I however need to pass the value in a const variable, myVar, into the browser context to perform comparisons within that context. However, I am running into a scoping issue when using $$eval. Is there a way around this?
Here is the code I made:
const myVar = 100;
const menuData = await page.$$eval("div#Menu", (Menu1) => {
return Menu1.map((Menu1Element) => {
console.log(myVar); //testing to see if myVar is passed into browser context
...
})[0];
}).catch(console.error);
The error message I get =>
Error: Evaluation failed: ReferenceError: myVar is not defined
This is the signature:
page.$$eval(selector, pageFunction[, ...args])
So you can pass args as pageFunction args (3rd parameter onwards in $$eval and these will be passed as args to your function.
This is the updated code snippet.
const myVar = 100;
const menuData = await page.$$eval("div#Menu", (Menu1, varInsideFunction) => {
return Menu1.map((Menu1Element) => {
console.log(varInsideFunction); //testing to see if myVar is passed into browser context
...
})[0];
}, myVar).catch(console.error);
As I saw in document (here), we can expose js function to page like this,
function do_many_operations(elem) {
let rect = elem.getBoundingClientRect();
return rect;
}
async function dummy_fn1(page) {
// Expose function here
await page.exposeFunction("do_many_operations", do_many_operations);
let temp = await page.evaluate(async (a, b) => {
let elems = document.querySelectorAll("p");
// Use function here
let rect = await do_many_operations(elems[0]);
}, 1, 2);
}
However, the code breaks with error message saying,
Error: Evaluation failed: TypeError: elem.getBoundingClientRect is not a function
That means the elem arg to do_many_operations is not a valid element or node, WHY??
Another question is, can we inject functions to browser context? So that we don't have to inject the function again and again for new pages created.
There is a serialization process when you call a function registered with exposeFunction.
When you call do_many_operations(elems[0]);, elems[0] is serialized (JSON.stringify) and passed to your function in Node. On the node side, that object won't have a getBoundingClientRect function.
If you want to reuse code you can create functions on the browser side using evaluate:
await page.evaluate(() => {
window.do_many_operations = function(elem) {
let rect = elem.getBoundingClientRect();
return rect;
}
});
I am using Puppeteer to parse a webpage and can't find a definitive answer to my question.
I am trying to pass a function as an argument to page.evaluate() An object is OK to pass but can't seem to pass a function. Here's a contrived example:
const obj = {
thing: 'thing1',
};
const myfunction = () => {
return `great ${stuff}`;
};
await page.evaluate((obj, function)=>{
const thing = myfunction;
},(obj, function));
Is it possible to pass a function as an argument to puppeteers page.evaluate()?
No, you cannot pass functions like that. The passed data needs to be serializable via JSON.stringify, which is not possible for functions.
Alternative: Expose the function
To use a function from your Node.js environment inside the page, you need to expose it via page.exposeFunction. When called the function is executed inside your Node.js environment
await page.exposeFunction('myfunction', text => `great ${text}`);
await page.evaluate(async (object) => {
return await window.myfunction(object); // 'great example'
}, 'example');
Alternative: Define the function inside page.evaluate
To use a function inside the page context, you can either define it inside of the context. This way, the function does not have access to your Node.js variables.
await page.evaluate((obj) => {
const myfunction = (stuff) => `great ${stuff}`;
return myfunction(obj); // 'great example'
}, 'example');