Search

Sunday, 10 March 2013

Sirius: adding Web core functionality

A lot of work were done for Win32 stuff starting from initial core functionality preparation and up to basic operations implementations as well as handling parent/child windows and user input. So, now we can work with Win32 (not in full extand but at least for base operations). One of the further expansion direction is the coverage of the Web functionality. That's the completely new area for Sirius Test Automation Platform and in this post I'll describe the core part creation which mainly affect server side.

Why covering Web?

Well, that the good question especially taking into account quite solid number of existing web testing tools. Firstly, not all those tools are free (and I'd say a lot of them quite expensive). Secondly, existing free tools are usually restricted with the technology domain support. So, Sirius is mainly targeted to cover both options. Firstly, it's for free and secondly it's not retricted with just Web so we should be able to combine Web application interaction and interaction with some other GUI types.

Eventually, I'm not going to reproduce initially the way to interact with web applications. I did similar stuff before while interacting with IE OLE object from Ruby (similar mechanism is used in Watir and WebDriver). So, now I'm mostly interested in getting functionality work. So, this time I'm not inventing the wheel, I'm just taking existing gear and just find the proper place to adjust it to the entire engine. This gear is the WebDriver and all I have to do is to wrap it within the Sirius platform.

Sirius server side development

Firstly I'll create new project for server side component and give it a name of Sirius-Server-Web. It should be another Maven project which shouldn't be different from any other Sirius server side projects created before except one more dependency entry in pom.xml file:

  <dependency>
   <groupId>org.seleniumhq.selenium</groupId>
   <artifactId>selenium-java</artifactId>
   <version>2.5.0</version>
  </dependency>
That will include libraries for the WebDriver.

After that I'll create the class for the WebDriver wrapper functionality which further will be the web service endpoint for the client side. I'll create the WebCore class in the org.sirius.server.web package with the basic content like:

package org.sirius.server.web;

public class WebCore {
 public WebCore() {
 }
 // TODO: Put the content here
}
Now we're ready for writing code.

WebDriver reference handling approach

When we wrap the WebDriver functionality we should always keep in mind the fact that WebDriver initializes the instance to communicate with web application. So, basically that's the variable which keeps the communication session information. So, we should store that variable all the time while Sirius server works or at least until we close the communication with WebDriver.

Another thing to be mentioned is that Sirius server works as web service and there may be multiple simultaneous connections to it with their own WebDriver sessions. So, we should keep multiple Web Driver instances. For this purposes we should reserve the storage for all those instances. So the WebCore class is updated with the field like:

public static HashMap<String, WebDriver> drivers = new HashMap<String, WebDriver>();
Now we should provide the functionality to fill this hash in with the data as well as cleaning from unused instances. All this functionality is done during start and stop operations for WebDriver instances. So, firstly we'll add method which creates new WebDriver instance. It should look like:
 public final String IE = "ie";
 public final String FIREFOX = "firefox";
 public final String CHROME = "googlechrome";
 public final String HTMLUNIT = "htmlunit";
 
 public String start(String browser) {
  WebDriver driver = null;
  if (browser.equalsIgnoreCase(IE)) {
   driver = new InternetExplorerDriver();
  } else if (browser.equalsIgnoreCase(FIREFOX)) {
   driver = new FirefoxDriver();
  } else if (browser.equalsIgnoreCase(CHROME)) {
   driver = new ChromeDriver();
  } else if (browser.equalsIgnoreCase(HTMLUNIT)) {
   driver = new HtmlUnitDriver();
  }

  if (driver != null) {
   drivers.put(driver.toString(), driver);
   return driver.toString();
  }

  return null;
 }
The highlighted part performs the driver information storage. It creates new instance and stores it in the hash map. The hash map key here is the hash code of the WebDriver instance which is quite unique. So, when we call any other methods which require WebDriver call we'll find appropriate driver using that key or token (this name will be used further). In order to minimize the code for getting proper driver we'll add some new method which gets required WebDriver instance by given token. The code looks like:
 private WebDriver driver(String token) {
  WebDriver driver = drivers.get(token);
  return driver;
 }
It will be intensively used further. The last step here is to add the functionality stopping the driver instance. Here it is:
 public void stop(String token) {
  driver(token).close();
  drivers.remove(token);
 }
In the highlighted part of the code we remove reference to WebDriver instance once it's stopped.

That's how we wrap the WebDriver itself.

Wrapping window related functionality

Next step is to add functionality interacting on window level. It's various navigation functions, switches to frames/windows etc. At this point we'll add the following methods:

 public void open(String token, String url) {
  driver(token).navigate().to(url);
 }

 public void back(String token) {
  driver(token).navigate().back();
 }

 public void forward(String token) {
  driver(token).navigate().forward();
 }

 public void refresh(String token) {
  driver(token).navigate().refresh();
 }

 public String getURL(String token) {
  return driver(token).getCurrentUrl();
 }

 public String getPageSource(String token) {
  return driver(token).getPageSource();
 }

 public String getTitle(String token) {
  return driver(token).getTitle();
 }

 public String getWindowHandle(String token) {
  return driver(token).getWindowHandle();
 }

 public void selectFrameByIndex(String token, int index) {
  driver(token).switchTo().frame(index);
 }

 public void selectFrameByName(String token, String name) {
  driver(token).switchTo().frame(name);
 }

 public void selectWindow(String token, String name) {
  driver(token).switchTo().window(name);
 }

 public void selectDefaultContent(String token) {
  driver(token).switchTo().defaultContent();
 }

 public void selectAlert(String token) {
  driver(token).switchTo().alert();
 }
These are just simple wrappers on existing WebDriver functionality.

Wrapping web elements related functionality

For web elements things are a bit more complicated for the following reasons:

  1. We need to pass the locator which is not the primitive type for WebDriver. So, we need the adaptor transforming primitive type representation (String type) to the one required by WebDriver.
  2. The element can be searched withing entire window or under some specific node. In WebDriver it's handled by findElementBy method but for different classes. That should be taken into account as well.
So, firstly we'll customize locators and apply their transformation to the format applicable to WebDriver. We'll use Selenium-style locators with small corrections and create our custom conversion method like:
 private By toLocator(String locator) {
  String prefix = locator.split("=")[0];
  String value = locator.substring(locator.indexOf("=") + 1);
  if (prefix.equals("id")) {
   return By.id(value);
  } else if (prefix.equals("name")) {
   return By.name(value);
  } else if (prefix.equals("link")) {
   return By.linkText(value);
  } else if (prefix.equals("tag")) {
   return By.tagName(value);
  } else if (prefix.equals("class")) {
   return By.className(value);
  } else if (prefix.equals("css")) {
   return By.cssSelector(value);
  } else if (prefix.equals("xpath")) {
   return By.xpath(value);
  }

  return null;
 }
After that we should wrap findElementBy functionality. We should pass the element locator and optionally the locator of the parent element. If parent locator is null we are looking for element within the entire window. Otherwise we look for parent element first and then for child element under it. The code implementing this logic looks like:
 private WebElement getElement(String token, String startFrom, String locator) {
  if (startFrom != null) {
   return driver(token).findElement(toLocator(startFrom)).findElement(
     toLocator(locator));
  }
  return driver(token).findElement(toLocator(locator));
 }
That's it. Once we have above 2 methods we can wrap major web element functionality. We should update our WebCore class with the following methods:
 public void clear(String token, String startFrom, String locator) {
  getElement(token, startFrom, locator).clear();
 }

 public void click(String token, String startFrom, String locator) {
  getElement(token, startFrom, locator).click();
 }

 public String getAttribute(String token, String startFrom, String locator,
   String attribute) {
  return getElement(token, startFrom, locator).getAttribute(attribute);
 }

 public String getCssValue(String token, String startFrom, String locator,
   String value) {
  return getElement(token, startFrom, locator).getCssValue(value);
 }

 public Point getLocation(String token, String startFrom, String locator) {
  return getElement(token, startFrom, locator).getLocation();
 }

 public Dimension getSize(String token, String startFrom, String locator) {
  return getElement(token, startFrom, locator).getSize();
 }

 public String getTagName(String token, String startFrom, String locator) {
  return getElement(token, startFrom, locator).getTagName();
 }

 public String getText(String token, String startFrom, String locator) {
  return getElement(token, startFrom, locator).getText();
 }

 public boolean isDisplayed(String token, String startFrom, String locator) {
  return getElement(token, startFrom, locator).isDisplayed();
 }

 public boolean isEnabled(String token, String startFrom, String locator) {
  return getElement(token, startFrom, locator).isEnabled();
 }

 public boolean isSelected(String token, String startFrom, String locator) {
  return getElement(token, startFrom, locator).isSelected();
 }

 public void sendKeys(String token, String startFrom, String locator,
   String text) {
  getElement(token, startFrom, locator).sendKeys(text);
 }

 public void submit(String token, String startFrom, String locator) {
  getElement(token, startFrom, locator).submit();
 }

Making the web service

After that the only thing left to do with server side is to add @WebService annotation on the class. Then we should add the WbCore class to the common configuration file for main runner just like it is described here. The entire source code can be found here.

Further steps

As before, once we have the server side developped we should generate the client side. After that we'll implement the client library interacting with the server side. But that's a matter of the separate post. But for now we've made an essential step on expanding Sirius Test Automation Platform to new technology.

No comments:

Post a Comment