I have a TypeScript project that uses Jest for unit tests and have just added Puppeteer to the mix with the intention to run some tests on the client. It works fine, unless I try use imported functions inside page.evaluate
.
For example, I have the following in HdpiCanvas.test.ts
:
import { createHdpiCanvas } from "./HdpiCanvas";
test("createHdpiCanvas", async () => {
await page.setViewport({ width: 800, height: 600, deviceScaleFactor: 2 });
let size = await page.evaluate(() => {
const canvas = createHdpiCanvas(); // document.createElement('canvas');
return [canvas.width, canvas.height];
});
console.log(size); // [600, 300] for HDPI canvas and [ 300, 150 ] for a regular one
});
With the commented out document.createElement('canvas')
the test runs just fine and logs [ 300, 150 ]
. But with the createHdpiCanvas()
the following error is thrown by the page.evaluate
function:
Error: Evaluation failed: ReferenceError: HdpiCanvas_1 is not defined
at __puppeteer_evaluation_script__:2:24
The actual createHdpiCanvas
inside HdpiCanvas.ts
is defined as follows:
export function createHdpiCanvas(width = 300, height = 150): HTMLCanvasElement {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
applyHdpiOverrides(canvas);
return canvas;
}
and itself depends on other functions defined in HdpiCanvas.ts
, like applyHdpiOverrides
.
The solution suggested by Meni Roytenburd is correct. If you don't like the fact that every function needs to be exposed to the browser separately, the only idea that comes to mind is transpiling your project to a single JavaScript file first and then injecting it as a <script>
tag — just like you would in real life.
The first step would be to generate a single JavaScript file with your bundle. Sometimes this can be accomplished with the TypeScript compiler alone, but a tool like Webpack can be used, too.
Once it's done, you can move your bundle to the client from within Puppeteer:
await page.addScriptTag({ path: 'path/to/the/bundle' });
Keep in mind this still may expose your functions to the global scope, hence accessing them via window
.
let size = await page.evaluate(() => {
const canvas = window.createHdpiCanvas();
return [canvas.width, canvas.height];
});
console.log(size); // [ 300, 150 ]
Another downside to this approach to having to deal with the warnings generated by TypeScript — at this point it doesn't know that createHdpiCanvas
exists on window
, so accessing window.createHdpiCanvas
will cause an error.
you can do like this before you invoke the evaluate
function :
await page.exposeFunction("applyHdpiOverrides",applyHdpiOverrides);
await page.exposeFunction("createHdpiCanvas",createHdpiCanvas);
and now your window will recognize these functions
As you may have figured it out already, the reason why page.evaluate
fails is because the function is evaluated in a different context and the current scope (where createHdpiCanvas
is available outside of the function) is lost.
I’m not familiar with pupeteer at all but by looking at the documentation for that function, there may be something you can try. It’s not pretty but perhaps it’s worth trying:
You could pass createHdpiCanvas
as a parameter, however a function is not serialisable so you would have to do it yourself:
page.evaluate((createHdpiCanvasCode) => {
const createHdpiCanvas = new Function(`return ${createHdpiCanvasCode}`)();
const canvas = createHdpiCanvas();
return [canvas.width, canvas.height];
}, createHdpiCanvas.toString());
However, you would have to inject your dependencies in the same way.