skip to Main Content

I want to create a class that wraps the Playwright’s Locator to add some utility functions.

The behavior of the locator should be exactly the same as the one provided by the testing framework, aside from the added utility functions.

However, if I implement the interface I have to re-implement all the functions manually, which is something I don’t want since the behavior doesn’t change. What is the proper way to do it?

Here some pseudo-code of what I want to do:

// I know I can't extend locator since its an interface but I'm using
// extends to better deliver the concept of what I want to do
export class LocatorWrapper extends Locator { 
 constructor (...args: any[]) {
    super(args);
 }
 
 public hoverAndDoubleClick = async () => {
   // This function is just a dummy
   await this.hover();
   await this.click();
   await this.click();
 }
}

2

Answers


  1. Chosen as BEST ANSWER

    To obtain an extended version of the locator I created a wrapper interface and then only used the locator inside my Page Object Model.

    // Locator extension
    export interface ExtendedLocator extends Locator {
      locator: (
        selector: string,
        options?: Parameters<Page['locator']>[1]
      ) => ExtendedLocator;
      myUtilityFunction: (
      ) => Promise<void>;
    }
    
    export const extendLocator = (
      locator: Locator,
    ): ExtendedLocator => ({
      ...locator,
      locator: (selector, options) =>
        extendLocator(locator.locator(selector, options)),
      myUtilityFunction: () => {/* EXEC SOME CODE */}
    });
    
    // POM
    const baseSelector = 'my-selector'
    export class PageObjectModel {
      private _locator: ExtendedLocator
    
      public constructor(page: Page){
        this._locator = extendLocator(page.locator(baseSelector))
      }
    
      public locator = (
        selector: string,
        options?: Parameters<Page['locator']>[1]
      ) => this._locator.locator(selector, options);
    
    }
    
    // USAGE
    test('Should verify a condition', () => {
      // get the page from playwright
    
      const page = new PageObjectModel(playwrightPage);
      await page.locator('another-selector').myUtilityFunc();
    })
    

    To make the creation of the POM even smoother a fixture can be used. I leave the link to the docs:


  2. How about to look into direction of using PageObject model and inheritance?

    You have base class which has abstract property mainSelector that is root. (It can be string or Locator as you prefer, or even you can create constructor for it)

    export abstract class PageObject {
        mainSelector: string;
    
        findLocator(selector: string, options?: Parameters<Page['locator'][1]>) {
           return page.locator(selector, options);
        }
    
        async shouldBeVisible() {
           return this.findLocator(this.mainSelector).waitFor({state: "visible"});
        }
     
        async hoverAndDoubleClick() {
            // your method
        }
    
       // other methods
    }
    

    and then each your new PageObject class would just extend from it.

    export class TestComponent extends PageObject {
       mainSelector: '.testRoot';
    }
    

    so TestComponent could use all methods from PageObjects.

    If you have contexts, you can adjust PageObject class to be context independent.

    export abstract class PageObject {
        constructor(public mainSelector: string, public context: Page | Frame = page) {}
    
        findLocator(selector: string, options?: Parameters<Page['locator']>[1]) {
           return this.context.locator(selector, options);
        }
    }
    

    This approach can help to easily wrap actions from Playwright and manipulate tab contexts by demand.

    Example of this approach on Puppeteer

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search