Skip to content

[Feature] Make it easier to reuse locators for chaining #13672

Closed

Description

@kenny-f

Let us know what functionality you'd like to see in Playwright and what your use case is.

Let's say I have a page object like the one below

class PageObject {
  readonly parent: Locator;

  readonly child: Locator;

  constructor(page: Page) {
    this.parent = page.locator('[data-id=parent]');
    this.child = page.locator('[data-id=child]');
  }
}

It makes sense in my use case to have these as separate locators.
But in some instances I would essentially want to chain the parent locator to the child locator.
I could add another locator this.parentChild = page.locator('[data-id=parent] [data-id=child]') but it seems like duplication of selectors that are already defined.
Or I could extract out the selectors but that again is more code.

Some ideas are:

  1. Allow the first arg of page.locator() to be either a selector: string OR a locator: Locator --> this.parent.locator(this.child)
  2. Be able to get the selector from a locator --> this.parent.locator(this.child.selector)

Activity

  1. imartinflores commented on Apr 21, 2022

    @imartinflores

    Can you not use something like:

    class PageObject {
      readonly parent: Locator;
    
      readonly child: Locator;
    
      constructor(page: Page) {
        this.parent = page.locator('[data-id=parent]');
        this.child = this.parent.locator('[data-id=child]');
      }
    }
    
  2. kenny-f commented on Apr 21, 2022

    @kenny-f
    Author

    Can you not use something like:

    class PageObject {
      readonly parent: Locator;
    
      readonly child: Locator;
    
      constructor(page: Page) {
        this.parent = page.locator('[data-id=parent]');
        this.child = this.parent.locator('[data-id=child]');
      }
    }
    

    That solves the problem if child is always under parent.
    But what if in some instances child is not under parent.
    Sorry, not good naming in the example.

  3. imartinflores commented on Apr 21, 2022

    @imartinflores

    It's alright. In that case, could you not use a string instead of a Locator object ?
    So you can create your selector, and then chain it or not depending if it's under the parent, i.e:

    class PageObject {
      readonly parent: Locator;
      readonly childSelector: String;
      readonly child: Locator;
    
      constructor(page: Page) {
        this.childSelector = "yourLocator";
        this.parent = page.locator('[data-id=parent]');
        this.child = page.locator(`${this.childSelector}`);
      }
    }
    

    And then if it's under, you can use:
    this.parent.locator(this.childSelector);

    The childSelector could also be a const instead, I assume you won't need to update it.

    Does it make sense ? Sorry if I am confused.

  4. kenny-f commented on Apr 21, 2022

    @kenny-f
    Author

    Does it make sense ? Sorry if I am confused.

    Yes, both your proposed solutions make sense and I have considered them.

    Maybe I wasn't clear enough as to why I'm requesting this feature.
    It's not that there is currently anything blocking me from achieving my use case.

    It's more of a convenience thing.
    It would be good if you could get away with only having locators in the page object instead of having a mix of locators and string selectors. Keeps the class smaller and cleaner IMO.

    Feel free to close if you don't think this is something that is worth pursuing.

  5. fbasso commented on Apr 25, 2022

    @fbasso

    My two cents, and another approach on this topic

    On our project, we are using this pattern (in its basic idea):

    class PageObject {
    
      constructor(private locatorRoot: Page | Locator) {
      }
    
      get locatorChild() {
          return this.locatorRoot.locator('[data-id=child]');
      }
    }

    So that you can instantiate your page object either with the page instance, or a parent locator.

  6. CtrlZvi commented on Aug 27, 2022

    @CtrlZvi

    Is your use case covered by Locator filtering? Specifically using the has filter with the child locator?

  7. kenny-f commented on Aug 30, 2022

    @kenny-f
    Author

    Is your use case covered by Locator filtering? Specifically using the has filter with the child locator?

    CtrlZvi Yes! Locator filtering will work!

    Thanks!

  8. crazyk2 commented on Mar 20, 2023

    @crazyk2
    class MyPageObject {
    
      page: Page;
      parent: Locator | undefined;
    
      constructor(page: Page, parent?: Locator) {
        this.page = page;
        this.parent = parent;
      }
    
      $parent = this.page.locator('#sidebar');
      $someButtonInMenu = this.getByTestId('menuButton');
      $menu = this.$parent.getByTestId('menuItem');
    
    async openMenu(buttonTitle: string, menuTitle?: string) {
    if (menuTitle)
        const menuLocator = this.$menu.filter({ hasText: menuTitle });
        menuLocator.click()
        menuLocator.locator(this.$someButtonInMenu.filter({ hasText: buttonTitle })).click(); // <--- desired way to chaining here
    } else {
        this.$someButtonInMenu.filter({ hasText: buttonTitle }).click(); // <--- desired way to chaining here
    }
    }

    I want to click on menuLocator and then on $someButtonInMenu.
    It would be great if I work only with locators (define them in top of class), but I have to mix locators and selectors (strings) now

    class MyPageObject {
    
      page: Page;
      parent: Locator | undefined;
    
      constructor(page: Page, parent?: Locator) {
        this.page = page;
        this.parent = parent;
      }
    
      $parent: Locator = this.page.locator('#sidebar');
      $someButtonInMenu: string = '[data-test="menuButton"]'; // String and locators ((((
      $menu: Locator = this.$parent.getByTestId('menuItem');
    
    async openMenu(buttonTitle: string, menuTitle?: string) {
    if (menuTitle) {
        const menuLocator = this.$menu.filter({ hasText: menuTitle });
        menuLocator.click();
        menuLocator.locator(this.$someButtonInMenu, { hasText: buttonTitle }).click(); // Different ways to work with elements on page
    } else 
        this.page.locator(this.$someButtonInMenu, { hasText: buttonTitle }).click(); // Different ways to work with elements on page
    }
    }

    I have a situation when someButtonInMenu need to be find inside parent element or vice versa without parent element

  9. dgozman commented on Mar 23, 2023

    @dgozman
    Collaborator

    Chaining locators is now possible with Locator.locator() method. Check out this guide.

  10. kenny-f commented on Mar 25, 2023

    @kenny-f
    Author

    Dmitry Gozman (@dgozman) amazing, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions