Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make WebDriver available from HTML Elements #56

Open
Actine opened this issue Apr 15, 2014 · 28 comments
Open

Make WebDriver available from HTML Elements #56

Actine opened this issue Apr 15, 2014 · 28 comments

Comments

@Actine
Copy link

Actine commented Apr 15, 2014

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

@aik099
Copy link
Contributor

aik099 commented Apr 16, 2014

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.

@Actine
Copy link
Author

Actine commented Apr 16, 2014

@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?

@aik099
Copy link
Contributor

aik099 commented Apr 16, 2014

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 👍

@artkoshelev
Copy link
Contributor

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

@paulakimenko
Copy link

Hi, guys. I need this feature too.

@Actine
Copy link
Author

Actine commented Jan 23, 2015

@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).

@paulakimenko
Copy link

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

@Actine
Copy link
Author

Actine commented Jan 24, 2015

@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 😃 )

@paulakimenko
Copy link

@Actine thank you. It works fine)

@andrew-sumner
Copy link

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'

@artkoshelev
Copy link
Contributor

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

@andrew-sumner
Copy link

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.

@aik099
Copy link
Contributor

aik099 commented Feb 27, 2016

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

@artkoshelev
Copy link
Contributor

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

@andrew-sumner
Copy link

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.

@artkoshelev
Copy link
Contributor

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.

@andrew-sumner
Copy link

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");
    }
}

@aik099
Copy link
Contributor

aik099 commented Mar 1, 2016

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: qa-tools/qa-tools#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.

@artkoshelev
Copy link
Contributor

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.

@Actine
Copy link
Author

Actine commented Mar 1, 2016

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.

@artkoshelev
Copy link
Contributor

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/).

@Actine
Copy link
Author

Actine commented Mar 1, 2016

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

@andrew-sumner
Copy link

@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
Copy link

@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;
    }
}

@artkoshelev
Copy link
Contributor

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

@andrew-sumner
Copy link

PR created :-)

@andrew-sumner
Copy link

PR now passes sonar checks...

@Actine
Copy link
Author

Actine commented Mar 3, 2016

СС @tmatveyeva

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants