puppeteer page.evaluate cannot exposeFunction with args - javascript

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;
}
});

Related

Converting a string to a function in JavaScript

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.

Is there a way to pass a global variable into an $$eval function?

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);

Is it possible to pass a function to Puppeteer's page.evaluate()

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');

Use function from .then method of Promise outside its scope

I have a file called paintTiming.js, which uses the Paint Timing API for finding out Web Performance paramters such as First Paint and First Contextual Paint. The file contents are shown below:
var FP, FCP, obj = [];
function parent() {
if ("PerformanceObserver" in window) {
let observerPromise = new Promise((resolve, reject) => {
// access the PerformanceObserver interface
let observer = new PerformanceObserver((list) => {
resolve(list);
});
observer.observe({
entryTypes: ["paint"]
});
}).then((list) => {
// Find out First Paint and First Contextual Paint
FP = list.getEntries()[0].startTime;
FCP = list.getEntries()[1].startTime;
// Store in array
obj[0] = FP;
obj[1] = FCP;
element = "The paint times are: <br> First Paint : " + FP + "ms, <br> First Contentful Paint : " + FCP + "ms";
// show values on web page
document.getElementsByTagName('p')[1].innerHTML = element;
}).then(() => {
// check if array is created, and is functioning
console.log(obj[0]);
}).then(() => {
// create function
function abc() {
return {
a: obj[0],
b: obj[1]
};
};
}).catch((error) => {
console.warn(error);
});
}
};
As you can see, a function abc() should be created when the promise is resolved.
My question : How do I use this function outside the promise?
For example, consider the sample below (used in a HTML file):
<script type="text/javascript" src="paintTiming.js"></script>
<script type="text/javascript">
// should return object containing FP and FCP values
console.log(abc());
</script>
The console shows that the function is not defined. If the JS code is inline, it still doesn't work.
You may not able to use the function if this is declared inside .then. Because this function abc is private to the callback function passed to .then & .then will only execute with the ajax.
Alternatively you can define this function outside the Promise and call this function inside the .then callback. Also need to bind the context using this

function as argument on evaluate method on google puppeteer

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.

Categories