SiKing

November 4, 2014

Selenium PageBuilder

Filed under: automation — SiKing @ 12:45 pm
Tags:

Anyone who has worked with Selenium for a while, will sooner or later try to find a way to organize all their element locators into some sort of a library. With a little bit of research you will be led to Selenium PageObject model, and subsequently the PageFactory pattern which makes your Page Objects a little less verbose.

In the Java world there is another interesting pattern: the Builder pattern. If your page is very predictable in how controls are created, then you might be able to use Page Builder to create all element locators on the fly. Here is how.

I write all my tests in Groovy, which is a scripting language that runs in a JVM. Big majority of things in the Groovy world are done with Builders. I was recently tasked with automating a Swagger page, which is a perfect candidate for the Page Builder pattern.

Swagger is a specification for documenting RESTfull APIs, and relies heavily on patterns and best practices. This makes the elements very easy to identify for Selenium. The page is divided into three groups of controls which Selenium can find like this:

class SwaggerBuilder {

	WebDriver driver
	def resources = [:]
	def operations = [:]
	def parameters = [:]

	SwaggerBuilder(WebDriver driver) {

		this.driver = driver
		buildResources()
	}

	def buildResources() {

		resources = driver.findElements(By.className("resource")).collectEntries {
			def resourceName = it.findElement(By.tagName("a")).getText().replaceFirst("[/]", "")
			[(resourceName): it]
		}
	}

	def buildOperations(WebElement resource) {

		operations = resource.findElements(By.className("operation")).collectEntries {
			def operationName = it.getAttribute("id")
			[(operationName): it]
		}
	}

	def buildParameters(WebElement operation) {

		parameters = operation.findElement(By.className("operation-params")).findElements(By.tagName("tr")).collectEntries {
			def parameterName = it.findElement(By.className("code")).getText()
			[(parameterName): it]
		}
	}
}

Another big deal in the Groovy world are Closures. In the code above, the .collectEntries is a Closure that returns a Map of my element locators. The rest of this is pretty straightforward Selenium.

So now you can start a test with something like:

def driver = new FirefoxDriver()
driver.get("http://petstore.swagger.wordnik.com/")
def petstore = new SwaggerBuilder(driver)

Right now our SwaggerBuilder() is only going to collect all the top-level resources; in the case of this example, it will be: pet, store, and user. We want to be able to expand a resource by calling something like: petstore.pet. But we do not want to explicitly define the parameter pet, because we want this to be usable for any Swagger page anywhere. This is where the magic of Groovy Builders comes in:

Object propertyMissing(String name) {

	if(resources[(name)] == null)
		throw new NoSuchElementException("Resource $name cannot be found.")

	if(!resources[(name)].findElement(By.className("endpoint")).isDisplayed())
		resources[(name)].findElement(By.linkText("List Operations")).click()

	buildOperations(resources[(name)])

	return this
}

Every time you try to call a property on petstore, that does not exist, Groovy will call the propertyMissing() method. The rest of the method above is, again, just Selenium: check if the resource has already been expanded or click on it to expand, and then build up the operations that are contained in it. So now in your test you can try something like:

def driver = new FirefoxDriver()
driver.get("http://petstore.swagger.wordnik.com/")
def petstore = new SwaggerBuilder(driver)
assertFalse(driver.findElement(By.id("pet_addPet")).isDisplayed())
petstore.pet
assertTrue(driver.findElement(By.id("pet_addPet")).isDisplayed())

And you will find that things are failing with stale element exceptions! This is because Swagger is heavily an AJAX page, and therefore Selenium needs a lot of hints when and where it needs to wait for stuff to be present.

First up is when the Swagger page loads up, it actually refreshes three times! So even using ExpectedConditions.refreshed() will not work. Before building the resources, either in the constructor or at the start of buildResources(), you need the following wait:

def wait = new FluentWait<By>(By.className("resource")).
	withTimeout(10, TimeUnit.SECONDS).
	pollingEvery(1000, TimeUnit.MILLISECONDS).
	ignoring(NoSuchElementException)
wait.until(new Function<By, Boolean>() {
		WebElement res
		Boolean apply(By by) {
			def oldRes = res
			res = driver.findElement(by)
			return res == oldRes
		}
	})

This will wait until the first resource is no longer changing. I had to raise the polling time to account for slow browsers; basically to fine tune this, it’s trial and error.

Now we want to be able to call one of the operations, something like: petstore.pet.pet_addPet(). Just as with the parameters above, we do not want to be explicitly declaring every single method.

Object invokeMethod(String name, Object args) {

	if(operations[(name)] == null)
		throw new NoSuchElementException("Operation $name cannot be found.")

	if(!operations[(name)].findElement(By.className("content")).isDisplayed())
		operations[(name)].findElement(By.tagName("a")).click()

	if(args.size() > 0) {
		buildParameters(operations[(name)])
	}
}

The invokeMethod() method specifies what do with methods that do not exist in Groovy, like the .pet_addPet() method above.

And again: Selenium will complain about things that are not there. First one to tackle is building the operations. Our method buildOperations() does not actually need the operations to be visible in the browser, because it is not interacting with them. But before we click on one, in the invokeMethod(), it needs to be visible. This is taken care of right after building the operations, either in propertyMissing() or in buildOperations() with a simple:

def wait = new WebDriverWait(driver, 5)
wait.until(ExpectedConditions.visibilityOfAllElements(operations.collect { it.value }))

And the exact same thing is repeated after we build up the parameters.

Now to submit the form, and wait for a response to come back:

Object invokeMethod(String name, Object args) {

	if(operations[(name)] == null)
		throw new NoSuchElementException("Operation $name cannot be found.")

	if(!operations[(name)].findElement(By.className("content")).isDisplayed())
		operations[(name)].findElement(By.tagName("a")).click()

	def wait = new WebDriverWait(driver, 5)
	if(args.size() > 0) {
		buildParameters(operations[(name)])
		wait.until(ExpectedConditions.visibilityOfAllElements(parameters.collect { it.value }))
		enterParameters(args[0])
	}

	operations[(name)].findElement(By.tagName("form")).submit()
	wait.until(ExpectedConditions.invisibilityOfElementLocated(By.className("response_throbber")))

	return assembleResponse(operations[(name)])
}

def enterParameters(Map args) {

	args.each {
		parameters[(it.key)].findElement(By.name(it.key)).sendKeys(it.value.toString())
	}
}

The parameters that any operation requires will be submitted as a Map. Not all operations have parameters, which is taken care of by the if statement. All that is left is to:

def assembleResponse(WebElement operation) {

	if(operation.findElements(By.className("error")).size() > 0)
		return null

	// wait for very large responses
	def wait = new WebDriverWait(driver, 20)
	wait.until(ExpectedConditions.visibilityOfAllElements(operation.findElements(By.className("block"))))

	def request_url = new URL(operation.findElement(By.className("request_url")).getText())

	def response_body
	def json = new JsonSlurper()
	def xml = new XmlSlurper()
	def response_class = operation.findElement(By.className("response_body")).getAttribute("class").split(" ")
	if(response_class.contains("json"))
		response_body = json.parseText(operation.findElement(By.className("response_body")).getText())
	else if(response_class.contains("xml"))
		response_body = xml.parseText(operation.findElement(By.className("response_body")).getText())
	else
		response_body = operation.findElement(By.className("response_body")).getText()

	def response_code = operation.findElement(By.className("response_code")).getText().toInteger()

	def response_headers = json.parseText(operation.findElement(By.className("response_headers")).getText())
		return ["request_url":request_url, "response_body":response_body, "response_code":response_code, "response_headers":response_headers]
}

The first if statement checks to make sure we supplied all required parameters. Some of the responses can be very large, so here I am using a 20 second wait. After that, just read everything and format it is a proper object: URL as URL(), JSON and XML as JsonSlurper() and XmlSlurper() – two canned Builders in Groovy – and integers as Integer.

The complete code of my SwaggerBuilder() can be found on my SourceForge account, along with some unit tests that show how to call and use this.

Advertisements

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: