How to use imported function inside page.evaluate

2020-07-09 06:07发布

问题:

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.

回答1:

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.



回答2:

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



回答3:

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.