Selenium: Get By from WebElement

Created on 5 Aug 2020  路  4Comments  路  Source: SeleniumHQ/selenium

馃殌 Feature Proposal

Extract the By locator from WebElement, in the similar idea we can extract the WebDriver from the WebElement.

Motivation

A Page Object can represent sections of a page as quoted from the implementation notes:

Finally, a PageObject need not represent an entire page. It may represent a section that appears many times within a site or page

Oftentimes an element goes stale after an action is performed. Re-initialising the element is an approach to address it but there is a repetition of the By locator.

Page Object

public class RbPage extends BasePage {

    public RbPage(WebDriver driver) {
        super(driver);
    }

    public ApplicationFormTab goToFormTab() {
        driver.findElement(By.id("formTab")).click();
        return new ApplicationFormTab(driver.findElement(By.cssSelector("div[ng-show='item.iAccept']")));
    }

}

Page Component

public abstract class BaseComponent {

    protected WebElement root;
    protected final WebDriver driver;
    protected final Actions actions;

    public BaseComponent(WebElement root) {
        this.root = root;
        this.driver = ((WrapsDriver) root).getWrappedDriver();
        this.actions = new Actions(driver);
    }

}
public class ApplicationFormTab extends BaseComponent {

    public ApplicationFormTab(WebElement root) {
        super(root);
    }

    public void save() {
        driver.findElement(By.id("btnSave")).click();
        root = driver.findElement(By.cssSelector("div[ng-show='item.iAccept']")); // Re-initialise root element, By locator is repeated in a separate location
    }

}

Example

The getBy() method extracts the By locator from the WebElement removing the duplication.

public void save() {
    driver.findElement(By.id("btnSave")).click();
    root = driver.findElement(root.getBy());
}
C-java I-question

Most helpful comment

Hey @kathyrollo, there's no way to get the locator from a WebElement that was originally used to locate it. That information just isn't retained as it isn't part of the W3C WebDriver spec. However, there are some pretty robust solutions for this issue that circumvent the need for it.

That link to the wiki, though, is pretty outdated and was made using overloaded terminology that we've found causes a lot of misconceptions. Here's a link to the current documentation that has an updated description with less confusing/overloaded terminology. The key difference is the introduction of the term "page component object".

One solution to the problem your facing is to have an interface that knows how to locate the WebElement, so it can be used to get a fresh reference every time you wish to do something with it, and then tossing away that reference. By getting a fresh reference every time, it's all but impossible to have a stale element reference (although, technically not impossible). These interfaces are what the "page component objects" are, and they can house more functionality to help keep your code a bit more "behavior" focused.

I found it to be very effective to create a standalone framework for building all of this out, rather than making all my page and component objects ad hoc. Here's a link to my framework's documentation, which might help demonstrate the concept a bit better.

But as a quick example, I could have something like landing_page.header.message.text which gets the text of the message element that's inside the header of the landing page. Both the header object and the message object are component objects, and each one can be used as an interface to reach the associated WebElement (so I could also do landing_page.header.is_displayed() for example). But no reference to the WebElements are acquired until I actually try to do something with them or get something from them (in this case, .text). It's only at that moment that my framework uses the locator to get the reference to the WebElement, does/gets what I want with/from it, and then tosses the reference away.

Frameworks like these also provide the perfect place to put waits so that whenever an action is performed, the wait can be put at the end of the function that did the action so it does the wait automatically whenever that function is called.

My framework is for Python, but there's ones for Java as well. Here's one I found after some searching (it's "element blocks" are effectively component objects):

https://github.com/yandex-qatools/htmlelements

Hope this helps! I'll be closing this issue, but please feel free to comment here (or make another ticket) if you're still having issues, or have other questions.

All 4 comments

Hey @kathyrollo, there's no way to get the locator from a WebElement that was originally used to locate it. That information just isn't retained as it isn't part of the W3C WebDriver spec. However, there are some pretty robust solutions for this issue that circumvent the need for it.

That link to the wiki, though, is pretty outdated and was made using overloaded terminology that we've found causes a lot of misconceptions. Here's a link to the current documentation that has an updated description with less confusing/overloaded terminology. The key difference is the introduction of the term "page component object".

One solution to the problem your facing is to have an interface that knows how to locate the WebElement, so it can be used to get a fresh reference every time you wish to do something with it, and then tossing away that reference. By getting a fresh reference every time, it's all but impossible to have a stale element reference (although, technically not impossible). These interfaces are what the "page component objects" are, and they can house more functionality to help keep your code a bit more "behavior" focused.

I found it to be very effective to create a standalone framework for building all of this out, rather than making all my page and component objects ad hoc. Here's a link to my framework's documentation, which might help demonstrate the concept a bit better.

But as a quick example, I could have something like landing_page.header.message.text which gets the text of the message element that's inside the header of the landing page. Both the header object and the message object are component objects, and each one can be used as an interface to reach the associated WebElement (so I could also do landing_page.header.is_displayed() for example). But no reference to the WebElements are acquired until I actually try to do something with them or get something from them (in this case, .text). It's only at that moment that my framework uses the locator to get the reference to the WebElement, does/gets what I want with/from it, and then tosses the reference away.

Frameworks like these also provide the perfect place to put waits so that whenever an action is performed, the wait can be put at the end of the function that did the action so it does the wait automatically whenever that function is called.

My framework is for Python, but there's ones for Java as well. Here's one I found after some searching (it's "element blocks" are effectively component objects):

https://github.com/yandex-qatools/htmlelements

Hope this helps! I'll be closing this issue, but please feel free to comment here (or make another ticket) if you're still having issues, or have other questions.

Hi @SalmonMode ,

Interesting "Page Component Object" is now mentioned in the new documentation, my current implementation actually uses component-based modelling where it targets a root element for a certain component. Page Objects can then return other Page Objects or Page Components as required. I added the parent class where every component is inheriting from for a better picture.

Thank you very much for your detailed response. So far it is not much a disadvantage as I only have 4 places where it needs the root element reload.

Awesome! And no problem!

If it helps, I believe I had a similar problem to the one you're describing, so I have the child components a means of asking their parent component ("parent" in the sense of composition, rather than inheritance, so "owner" may be more appropriate) for their associated WebElement so the child component could search within that for its own associated WebElement, rather than the entire DOM.

For example, if other had _find_from_parent set to True, when I call page.thing.other.is_displayed() it would more or less doing something like this behind the scenes:

parent_el = page.thing.other._parent._get_element()
other_el = parent_el.find_element(page.thing.other._locator)
other_el.is_displayed()

So it gives me a sort of by-chaining.

Also, because they ask their parents for their WebElement instead of just grabbing their locator, the effect can cascade upward. So if I do page.thing.other.another.is_displayed(), and both other and another have _find_from_parent set to True, another will have to ask other for its WebElement before it can find its own, but other will have to first ask thing for its associated WebElement to find its associated WebElement.

I got lucky with Python descriptors, as I can grab a reference to the parent component/page as child component is referenced, e.g. when I do page.thing.other, thing can grab a reference to page right when thing is evaluated, and the same is true for other getting a reference to thing. The child components figure out who their parent component is at runtime when they get referenced (because of the descriptor __get__ and __set__ methods), so I don't need to pass that info in myself when I attach them to their parents.

There's also no caching so everything is fetched on every reference as needed (avoids stale element exceptions and helps with iterable structures that change).

I'm not sure what an implementation of that would look like in Java though.

Hi @SalmonMode ,

Nice! Our by-chaining technique isn't too far off.

Page Component

public class ApplicationFormTab extends BaseComponent {

    public ApplicationFormTab(WebElement root) {
        super(root);
    }

    public void save() {
        // same method
    }

    public PaymentTypesForm getPaymentTypesForm() {
        return new PaymentTypesForm(root.findElement(By.tagName("initial-payment")));
    }

}

Nested Page Component

public class PaymentTypesForm extends ApplicationFormTab {

    public PaymentTypesForm(WebElement root) {
        super(root);
    }

    public Double getInitialPayment() {
        return Double.parseDouble(
                Strings.stripNonNumeric(root.findElement(By.id("initialPaymentAmount")).getAttribute("value")));
    }

}

This is a page component within a page component (nested). It finds its target element from the component's root element. It traverses elements downward (owner > component > nested component) probably akin to your page > thing > other. Page Components can return other Page Components or Page Objects as required.

For traversing upward, I use the extracted driver from the BaseComponent that is why every component that extends BaseComponent still has access to the driver.

Interesting thing you got going with Python descriptors. I think the Java equivalent is also worth checking out if there is. :)

Was this page helpful?
0 / 5 - 0 ratings