7

I would like to add custom methods to the puppeteer.Page object, so I could invoke them like so:

let page = await browser.newPage();
page.myNewCustomMethod();

Here is one out of many custom methods I have created. It finds first available element by the XPath expression, using the array of expressions:

const findAnyByXPath = async function (page: puppeteer.Page, expressions: string[]) {
    for (const exp of expressions) {
        const elements = await page.$x(exp);

        if (elements.length) {
            return elements[0];
        }
    }

    return null;
}

I have to invoke it like so...

let element = await findAnyByXPath(page, arrayOfExpressions);

To me, that looks weird in the editor, especially in a region where many custom methods are being invoked. It looks to me, a bit of "out of context". So I would rather invoke it like that:

page.findAnyByXPath(arrayOfExpressions);

I'm aware that there is a page.exposeFunction method, but it is not what I'm looking for.

What is a way to achieve this?

NearHuscarl
  • 66,950
  • 18
  • 261
  • 230
RA.
  • 969
  • 13
  • 36

2 Answers2

6

Can you do this? Yes.

You can extend any object in JavaScript by modifying its prototype. In order to add a function to a Page object, you can access the prototype of a Page object by using the __proto__ property.

Here is a simple example adding the function customMethod to all Page objects:

const page = await browser.newPage();
page.__proto__.customMethod = async function () {
    // ...
    return 123;
}
console.log(await page.customMethod()); // 123

const anotherPage = await browser.newPage();
console.log(await anotherPage.customMethod()); // 123

Note, that you need a Page object first, to access the prototype as the constructor function (or class) is not itself exposed.

Should you do this? No.

You probably already noticed the red warnings on the linked MDN docs above. Read them carefully. In general, it is not recommended to change the prototype of objects you are using and haven't created yourself. Someone has created the prototype and he did not expect anyone to tinker around with it. For further information check out this stackoverflow question:

How to do it instead?

Instead, you should just use your own functions. There is nothing wrong with having your own functions and call them with page as argument like this:

// simple function
findAnyByXPath(page);

// your own "namespace" with more functionality
myLibrary.findAnyByXPath(page);
myLibrary.anotherCustomFunction(page);

Normally, you could also extend the class Page, but in this case the library is not exporting the class itself. Therefore, you can only create a wrapper class which executes the same functions inside but offers more functionality on top. But this would be a very sophisticated approach and is really worth the effort in this case.

Thomas Dondorf
  • 23,416
  • 6
  • 84
  • 105
  • Thanks a lot. I'm new to JS world and learning everyday. Is there a better solution then? Like maybe extending the `Page` class by using own, custom class `MyPage` and have custom methods there? – RA. Jul 12 '19 at 18:06
  • @RA. You cannot extend `Page` as this is not exposed by the library. I added more information the answer. Hope it helps you :) – Thomas Dondorf Jul 12 '19 at 19:12
2

To expand on @Thomas's answer, if you want to override an original method of Page:

const extendPage = (page: Page) => {
  const { goto: originalGoto } = page;

  page.goto = function goto(url, options) {
    console.log("Goto:", url);
    // do your things
    return originalGoto.apply(page, arguments);
  };

  return page;
};
const page = extendPage(await browser.newPage());

await page.goto("https://google.com"); // Goto: https://www.google.com

To attach additional methods every time a new Page is created, you can listen to the targetcreated event from the Browser and extend the page in the callback:

const browser = await puppeteer.launch();

browser.on("targetcreated", async (target: Target) => {
  if (target.type() === "page") {
    const page = await target.page();
    extendPage(page);
  }
});

const page = await browser.newPage(); // extended page

If you want to add a new method and update Typescript definition:

import { Page, PageEmittedEvents } from "puppeteer";

async function htmlOnly(this: Page) {
  await this.setRequestInterception(true); // enable request interception

  this.on(PageEmittedEvents.Request, (req) => {
    if (req.resourceType() === 'document') return req.continue();
    return req.abort();
  });
}

declare module "puppeteer" {
  interface Page {
    htmlOnly: () => Promise<void>;
  }
}

export const extendPage = (page: Page) => {
  page.htmlOnly = htmlOnly;
  return page;
};
browser.on("targetcreated", async (target: Target) => {
  if (target.type() === "page") {
    const page = await target.page();
    extendPage(page);
  }
});

const page = await browser.newPage();

await page.htmlOnly();
NearHuscarl
  • 66,950
  • 18
  • 261
  • 230