htmlelements
htmlelements copied to clipboard
Make WebDriver available from HTML Elements
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
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 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?
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:
@Actine feel free to send you PR implementing this issue, didn't see any problems here
Hi, guys. I need this feature too.
@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 , sorry, could you send example if you have it?
@paulakimenko here's the idea
- 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();
}
}
- Add this class to your project (is a copy-paste of
HtmlElementDecorator
andHtmlElementListNamedProxyHandler
classes with tweaks): https://gist.github.com/Actine/05698ecd32d29cac69f9 - 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 thank you. It works fine)
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 can you describe problem you solving please?
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.
Why not use @Timeout
annotation on the element itself to specify that element might not be immediately available?
And even without @Timeout
annotation you don't need to use waiter, default timeout for searching elements is 5 seconds
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.
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.
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");
}
}
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.
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.
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.
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 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.
@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?
@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 so, how about proposing PR with your feature implementation? =)
PR created :-)
PR now passes sonar checks...
СС @tmatveyeva