htmlelements icon indicating copy to clipboard operation
htmlelements copied to clipboard

Make WebDriver available from HTML Elements

Open Actine opened this issue 10 years ago • 28 comments

Hello Yandex,

We are developing a test framework for our rich web application using Java, Selenium, Spring for dependency management, and HTML Elements for defining layouts. We employ a strict separation of concerns where Pages, Elements, and nested Elements not only declare the composition, but methods to interact with them (somewhat similar to Views in Backbone.Marionette MVC). This way they provide an API to the higher level but never expose the elements themselves. In these methods we sometimes need to have a current WebDriver instance (e.g. to perform double-click or other non-standard Actions), that is, the same WebDriver that was used by Page Object Factory to initialize the elements. Currently the HTML Element entities (Buttons, Links etc) are not aware of WebDriver that initialized them, so we have to pass its instance to those methods explicitly, which is not a very nice workaround:

@Name("Tree Item")        // used in Tree (also HtmlElement) reused on many pages
@Block(@FindBy(xpath = "/li"))
public class TreeItem extends HtmlElement {
    @FindBy(xpath = "./p")
    private WebElement label;
...
    public void doubleClick(WebDriver driver) {
        new Actions(driver).doubleClick(this.label).perform();
    }
...
}

Could you please consider to make WebDriver available, so that we could get it in a fashion like this:?

@Name("Tree Item")
@Block(@FindBy(xpath = "/li"))
public class TreeItem extends HtmlElement {
...
    public void doubleClick() {
        new Actions(this.getDriver()).doubleClick(this.label).perform();
    }
...
}

Thank you in advance, With best regards, ~Actine

Actine avatar Apr 15 '14 22:04 Actine

You can access the underlying WebElement via this.getWrappedElement() construct. If the Selenium's WebElement has that reference, then you can get it as well.

new Actions(this.getDriver()).doubleClick(this.label).perform();

Are you sure you can't act directly on the element, like this.label.doubleClick()?. Maybe I'm mistaking this approach with one I've implemented in my PHP fork of the HtmlElements (see https://github.com/aik099/qa-tools) however.

aik099 avatar Apr 16 '14 06:04 aik099

@aik099 I know about this.getWrappedElement(), however it doesn't store the reference to WebDriver (IIRC it doesn't even hold a reference to parent SearchContext). That's why I'm suggesting adding this to HtmlElement and propagate down the element hierarchy upon initialization.

Are you sure you can't act directly on the element, like this.label.doubleClick()?

No, there's no such capability. Besides, we might need more complex actions there that we'll have to build using Actions builder.

Besides, Apache 2.0 allows to modify sources and use them in non-Apache project, right? so that we can fork HTML Elements and implement this ourselves, in case there's no positive output from the devs?

Actine avatar Apr 16 '14 08:04 Actine

Besides, Apache 2.0 allows to modify sources and use them in non-Apache project, right? so that we can fork HTML Elements and implement this ourselves, in case there's no positive output from the devs?

Better not to go that road. I suggest you sending a PR with proposed functionality. I personally don't think that exposing driver used to build up the TypifiedElement will harm anybody or doesn't conform to library's idea.

So :+1:

aik099 avatar Apr 16 '14 08:04 aik099

@Actine feel free to send you PR implementing this issue, didn't see any problems here

artkoshelev avatar Apr 16 '14 12:04 artkoshelev

Hi, guys. I need this feature too.

paulakimenko avatar Jan 23 '15 10:01 paulakimenko

@paulakimenko the truth is, I implemented this in my project long ago. It required implementing own decorators and a list proxy, which ended up with a lot of copy-paste from the library's code (because of #68). I'm just too lazy to make pull requests, mostly because it requires additional effort (supplying unit tests).

Actine avatar Jan 23 '15 10:01 Actine

@Actine , sorry, could you send example if you have it?

paulakimenko avatar Jan 23 '15 14:01 paulakimenko

@paulakimenko here's the idea

  1. Create an interface like below, and implement it in your base html element class (if you have it), or the concrete classes
public interface WebDriverAware {
    public void setWebDriver(WebDriver driver);
}
public class MyPageComponent extends HtmlElement implements WebDriverAware {
    private WebDriver driver;
    public void setWebDriver(WebDriver driver) { this.driver = driver; }
    ...
    public void doubleClick() {
        new Actions(this.driver).doubleClick(this.label).perform();
    }
}
  1. Add this class to your project (is a copy-paste of HtmlElementDecorator and HtmlElementListNamedProxyHandler classes with tweaks): https://gist.github.com/Actine/05698ecd32d29cac69f9
  2. Now when you initialize your block, use your new decorator and make sure that you don't forget to inject webdriver into it. For example, create this factory method wherever you have access to your webdriver:
public class MyFactory {
    private WebDriver driver;
    public MyFactory(WebDriver driver) { this.driver = driver; }
...
    public <T extends WebElement> T initPage(Class<T> objectClass) {
        try {
            T object = objectClass.newInstance();
            WebDriverAwareDecorator decorator = new WebDriverAwareDecorator(driver);
            decorator.setWebDriver(driver);
            PageFactory.initElements(decorator, object);
            return object;            
        } catch (InstantiationException e) {
            // Use some custom exception rather than generic RuntimeException
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
// somewhere around starting a new session
MyFactory myFactory = new MyFactory(driver);

// somewhere in test
MyPage page = myFactory.initPage(MyPage.class);
page.getMyComponent().doubleClick();  // should use the recursively injected driver

Hope this helps. Adjust to your architecture. Please reply whether you got it working (I trimmed down irrelevant stuff that we have in our implementation, might not compile at first :smiley: )

Actine avatar Jan 24 '15 00:01 Actine

@Actine thank you. It works fine)

paulakimenko avatar Jan 24 '15 07:01 paulakimenko

I need to use WebDriverWait in my html element but am unable to do this currently because the constructor requires a WebDriver object.

Is there any chance you can implement this functionality?

I'm using 'ru.yandex.qatools.htmlelements:htmlelements-java:1.15'

andrew-sumner avatar Feb 26 '16 12:02 andrew-sumner

@andrew-sumner can you describe problem you solving please?

artkoshelev avatar Feb 26 '16 13:02 artkoshelev

I have a responsive web application with two different navigation menu's: one for phone layout and one for desktop layout. I'm using the HtmlElement as a wrapper for the navigation menu but I need to put a wait command in to ensure that one or the other menu has finished loading and is visible before attempting to access the WebElement and would prefer to use a WebDriverWait command which requires access to the underlying WebDriver.

andrew-sumner avatar Feb 27 '16 11:02 andrew-sumner

Why not use @Timeout annotation on the element itself to specify that element might not be immediately available?

aik099 avatar Feb 27 '16 11:02 aik099

And even without @Timeout annotation you don't need to use waiter, default timeout for searching elements is 5 seconds

artkoshelev avatar Feb 27 '16 11:02 artkoshelev

I hadn't seen the @Timeout annotation before, but found it buried in the release notes. It may meet this requirement, I'll have to look into the implementation a bit more first.

Recommended advice is not to mix explicit and implicit waits (http://www.seleniumhq.org/docs/04_webdriver_advanced.jsp), we perform explicit waits using WebDriverWait when needed. What does the @Timeout annotation do behind the scenes?

My second scenario (not previously mentioned) is that sometimes I'd like to call the findElement() method on the driver so that I can use a more customised selector. Eg in a calendar control I'd like to wrap in an HtmlElement I currently use driver.findElement(By.cssSelector("li[data-date='" + date + "']")); to return the element for a specific date.

In the same way that the original poster did not want to pass the driver into the method so that he can call an action, I don't want to pass in the driver just to call findElement when the HtmlElement presumably already knows what driver is being used.

andrew-sumner avatar Feb 27 '16 22:02 andrew-sumner

What does the @Timeout annotation do behind the scenes?

it's an element-specific implicit wait

I'd like to call the findElement() method on the driver so that I can use a more customised selector

I'll not recommend to do any direct driver calls since you introduce PageObject architecture in your project. In your specific case you can describe calendar items as collection and then iterate to find one with required date. Using lamdaj library and matchers will help a lot here.

artkoshelev avatar Feb 28 '16 07:02 artkoshelev

I certainly understand that you want to keep to a pure page object pattern, unfortunately for me that comes at the cost of some flexibility in how I can use your very awesome project.

How would you suggest I do this one then? (I just encountered this issue today)

The below class is a block element that represents a menu. The web pages its used in contain iframes, although this menu it not in a frame. I don't want to have to worry about the iframes for the menu item in any of my pages, and I don't want to have to pass the driver into the component as in my example below.

In my page object I'd like to declare the component as a class variable and have the page factory instantiate it:

UserMenuComponent userMenu;

And use it like this (the navigate using method places a red border around the element and takes a screen shot for documentation/debugging):

navigateUsing(userMenu.getLogoutMenuItem());

My class

@FindBy(css = ".processPortalBanner #processPortalUserDropdownId")
public class UserMenuComponent extends HtmlElement {

    @FindBy(xpath = "//div[@id='processPortalUserDropdownId_dropdown']//tr[contains(@class, 'dijitMenuItem')]")
    List<WebElement> menuItems;

    private WebElement getMenuItem(String label) {
        for (WebElement menuItem : menuItems) {
            if (menuItem.getText().equals(label)) {
                return menuItem;
            }
        }

        throw new NoSuchElementException("Menu item " + label + " was not found");
    }

    //TODO I don't want to pass in the WebDriver here...
    public WebElement getLogoutMenuItem(WebDriver driver) {
        driver.switchTo().frame(0);
        driver.switchTo().defaultContent();

        getWrappedElement().click();

        return getMenuItem("Logout");
    }
}

andrew-sumner avatar Mar 01 '16 01:03 andrew-sumner

So the main problem is transparent switching between frames the elements are located in, when those elements are accessed. I also have exact same problem in my PHP version of this library: https://github.com/qa-tools/qa-tools/issues/116

The problem, in my case, if that reference to an element (that might itself be in a frame) isn't kept, when element is found. Although it might be really cool to allow referencing frames in xpath of the element.

aik099 avatar Mar 01 '16 08:03 aik099

Yep, handling iframes is a completely different (and pretty complex) story. Implementing it as element annotation will require checking current frame for every element call, which will slow down test execution unpredictably. @andrew-sumner in your specific case i would implement @iframe annotation for high-level test steps, which will handle frame switching before and after method execution.

artkoshelev avatar Mar 01 '16 09:03 artkoshelev

Original poster here. I'm not doing Selenium automation for a year already, but seeing the issue being brought up again I decided to jump into the discussion.

@artkoshelev sorry but it seems that you're trying to avoid implementing the requested feature at all cost :) suggesting suboptimal workarounds and new annotations only to not expose the driver. However, as far as I understand, the use case in my original message (building Actions such as double-click or context click on an element) is still not addressed.

@andrew-sumner In your case with a date picker you don't necessarily need a driver to find an element with customized selector. Any WebElement is a SearchContext, so you can use myCalendar.findElement(...) instead of driver.findElement(...). And in case you need to select immediate children or go up the element hierarchy, you can do this with xpath (unlike css where you can't).

Also there's still my solution from the comment above. Now that #68 is addressed, you don't have to copy-paste those two classes but extend and override a few methods.

Actine avatar Mar 01 '16 15:03 Actine

Need for webdriver instance inside elements is a smell of bad test architecture for me. WebElements and HtmlElements were created to describe page structure, not actions. To implement actions which need webdriver just use objects which already knows about webdriver - pages or steps (see http://www.thucydides.info/#/ or http://allure.qatools.ru/).

artkoshelev avatar Mar 01 '16 16:03 artkoshelev

@artkoshelev then why WebElements/HtmlElements have methods like click() and sendKeys()? We can myElement.click(), so why can't we make myElement.contextClick() (which needs actions unfortunately) but have to use steps per your suggestion? This is a smell of inconsistency.

I agree that the use of WebDriver instance in page objects / components must be put to the minimum if not avoided completely. But sometimes it's justified.

Actine avatar Mar 01 '16 17:03 Actine

@artkoshelev I'm not convinced that I would use an @iframe annotation, i'd prefer to handle that myself. You state "which will handle frame switching before and after method execution", I might want that behaviour in some cases, in other's (like my example above) I don't want the frame to switch back after execution.

Same applies to the @time annotation as well - I'm not sure that I'm ever likely to use it as we have a policy against using implicit waits and only use explicit waits as required.

If you won't supply the webdriver I guess I will either continue to pass in the webdriver as required or look at the solution @Actine mentioned.

I disagree with your above statement that "need for webdriver instance inside elements is a smell of bad test architecture". In some cases it's the only way to interact with some complex web elements, for example: have you ever tried automating a Dojo based web application?

andrew-sumner avatar Mar 01 '16 20:03 andrew-sumner

@Actine Thanks for the suggestion - I've implement it and it was so easy given the changes in the latest version of htmlelements. Here's my implementation for anyone else that may have this problem:

@artkoshelev While I believe this functionality should be part of the project, I'm very impressed with how easy it was to extend htmlelements to meet my requirement - so a big thanks to everyone who have worked to develop this tool.

Create these classes

public interface WebDriverAware {
    public void setWebDriver(WebDriver driver);
}
public class WebDriverAwareDecorator extends HtmlElementDecorator {
    private WebDriver driver;

    public WebDriverAwareDecorator(CustomElementLocatorFactory factory, WebDriver driver) {
        super(factory);

        this.driver = driver;
    }

    @Override
    protected <T extends HtmlElement> T decorateHtmlElement(ClassLoader loader, Field field) {
        T element = super.decorateHtmlElement(loader, field);

        if (element instanceof WebDriverAware) {
            ((WebDriverAware)element).setWebDriver(driver);
        }

        return element;
    }
}

Construct page object

PageFactory.initElements(new WebDriverAwareDecorator(new HtmlElementLocatorFactory(driver), driver), this);

Example Implementation

public class MyComponent extends HtmlElement implements WebDriverAware {
    WebDriver driver = null;

    @Override
    public void setWebDriver(WebDriver driver) {
        this.driver = driver;
    }
}

andrew-sumner avatar Mar 01 '16 22:03 andrew-sumner

@andrew-sumner so, how about proposing PR with your feature implementation? =)

artkoshelev avatar Mar 02 '16 10:03 artkoshelev

PR created :-)

andrew-sumner avatar Mar 03 '16 02:03 andrew-sumner

PR now passes sonar checks...

andrew-sumner avatar Mar 03 '16 21:03 andrew-sumner

СС @tmatveyeva

Actine avatar Mar 03 '16 21:03 Actine