javascripttypescriptmethodspuppeteerextending

Adding a custom method to puppeteer.Page object


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?


Solution

  • 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.