Skip to content

Custom Exporter Developer Documentation

Zarkonnen edited this page Aug 3, 2012 · 2 revisions

Exporters are modules that convert a Selenium 2 script into a string representation. The default representation of a script is as a JSON structure, but scripts can also be exported in a variety of programming languages, for example Python or Java.

New exporters can be registered by pushing them onto builder.selenium2.io.formats.

Raw exporters

An exporter is an object that must define the following properties:

  • name (string) Name of the format (eg "Python")
  • extension (string) Extension of the exported file (eg ".py")
  • format (function(script:builder.Script, name:string) -> string) Formats a builder.Script into a string.
  • canExport (function(stepType:builder.selenium2.stepTypes.*) -> boolean) Whether the given step type can be exported.
  • nonExportables (function(script:builder.Script) -> list of string) Given a script, produce a list of the names of step types in the script that cannot be exported.

This very simple raw exporter outputs the step types of the script:

builder.selenium2.io.formats.push({
  name: "StepTypes",
  extension: ".txt",
  format: function(script, name) {
    var out = name + ":\n";
    for (var i = 0; i < script.steps.length; i++) {
      out += script.steps[i].type.getName() + "\n";
    }
    return out;
  },
  canExport: function(stepType) { return true; },
  nonExportables: function(script) { return []; }
});

Exporter considerations

To export a script into a programming language, the exporter needs to output the following things:

  • Setup: creating the webdriver instance, etc.
  • Teardown: closing the instance, etc.
  • Mapping step types onto code fragments.
  • Escaping parameters: eg "foo" has to become "\"foo\"".
  • Mapping different locator strategies onto code fragments
  • Dealing with negated steps. Assert/Verify/Wait steps can be negated. For example, you can assert that some text is present, or you can assert that the text is not present. In Selenium 1 this is handled using two separate step types. In Selenium 2 - and internally in Builder - this is handled by having a negated flag on steps.
  • Variable storage: Store steps need to be mapped to variable assignment.
  • Variable substitution: The ${varname} syntax has to be turned into appropriate expressions. For example, foo${bar} has to become "foo" + bar.

Generated exporters

To make creating an exporter easier, Builder has the function builder.selenium2.io.createLangFormatter, which takes a set of information about an export language and processes it into a formatter that can then be added using builder.selenium2.io.formats.push.

The information required is a JS object with the following fields. Each part of the process will be explained in detail below.

  • name (string) Name of the format (eg "Python")
  • extension (string) Extension of the exported file (eg ".py")
  • start (string) Text at the start of the exported string
  • end (string) Text at the end of the exported string
  • escapeValue (function(stepType, value, pName) -> string) Correctly escapes a parameter value
  • locatorByForType (function(stepType, locatorType, locatorIndex) -> string) Returns code needed for a particular locator strategy
  • usedVar (function(varName, varType) -> string) Returns code for accessing previously defined variable
  • unusedVar (function(varName, varType) -> string) Returns code for setting new variable
  • not (string) The negation operator, eg ! or not
  • lineForType (map of string to string/function(step, escapeValue)) Maps the names of step types onto strings with a substition syntax, or in rare cases onto functions that process the step into text.

Starting and ending

The exported string is built up bit by bit. The start text is the first thing added. Then, each step in the script is processed and appended. Finally, the end text is appended.

Step type mapping

Steps are looked up in lineForType by name. If the resulting object is a string, the step parameters and any negation tokens are substituted in, and the result is appended to the exported string being built.

If the resulting object is a function, it's called with the step to be processed and the escapeValue function as parameters. It should return a string which is then directly appended with no substitution being done by the exporter system.

Step parameter substitution

Curly brace expressions like {url} are replaced with the parameter value of that name. For example, in Python, the get step type is mapped by lineForType onto

wd.get({url})

If the url parameter of a get step is http://sebuilder.com, substitution produces the following line:

wd.get("http://sebuilder.com")

Note the quotes, which were added by escapeValue.

Escaping

Different languages have different string syntaxes, so each exporter needs a function, escapeValue, that can turn a raw string into the correct expression for the export language. escapeValue should put quotes around the string, escape any expressions (such as other quotes) that need escaping, and substitute in variable values (see below for more details).

Examples (in Python):

  • foo -> "foo"
  • "kitten" -> "\"kitten\""
  • ${title} -> title
  • Bob ${lastname} -> "Bob " + lastname

Locator Formatting

Different locator strategies need different code chunks. For example, if you want to locate by id in Python, you use the find_element_by_id function, and if you want to locate by xpath it's find_element_by_xpath.

The substitution syntax for locator strategies is to append By at the end of the parameter name. For example, {locator} gets substituted with the locator's value (eg "submit_button"), and {locatorBy} with the code fragment for the locator strategy (eg find_element_by_id). Locator values are escaped using escapeValue.

Most of the time, which code fragment to use is determined entirely by the locator type, but there can be individual step types that require different code. For example, in Python, assertElementPresent and verifyElementPresent require the find_elements function rather than the find_element ones.

The locatorByForType function is called with the name of the step type, the locator type, and the index of the locator parameter. The step type could for example be clickElement and the locator type can be any of class, id, link text, xpath, css selector and name. Finally, the locatorIndex allows you to return different text for different locator parameters in the same step type.

So in Python, calling locatorByForType("clickElement", "id", 0) returns find_element_by_id. The string for clickElement is wd.{locatorBy}({locator}).click(), which after substitution becomes wd.find_element_by_id("submit_button").click.

Negation, posNot and negNot

Code generated for negated steps has to invert some of its tests. To support this without having to create two versions of the code for the negated and non-negated cases, there are two bits of substitution syntax that can be used.

{negNot} is substituted with the exporter's not value if the step is negated, and substituted with the empty string if the step is not negated.

{posNot} is substituted with the empty string if the step is negated, and substituted with the exporter's not value if the step is negated.

For example, the string for verifyCurrentUrl in Python is

if {posNot}(wd.current_url == {url}):
    print(\"{negNot}verifyCurrentUrl failed\")

In this case, since we want to print a warning if the current URL does not match the supplied URL, there should be a not before the condition in the normal case. If we want to verify the reverse, that the current URL does not match the supplied URL, the not is left out.

In the warning message, we want the not to show up in the negated case to indicate that the negated case was tested.

So in the non-negated case, this becomes:

if not (wd.current_url == {url}):
    print(\"verifyCurrentUrl failed\")

And in the negated case:

if (wd.current_url == {url}):
    print(\"not verifyCurrentUrl failed\")

(Note that the not value in the Python exporter is "not ", including a trailing space.)

Storing Variables

Selenium supports storing values in variables. Storing is done using the store commands. To translate these correctly, there is a substitution syntax to produce variable assignment code.

Expressions of the form ${{varName}:varType} are processed by calling either unusedVar or usedVar. The former is called if this is the first time a variable expression with this varName is encountered. The latter is called otherwise.

The system looks up the name of the variable in the parameter given by {varName}, but does not escape it. The name of the variable and its type are supplied to the unusedVar or usedVar function. The varType is optional.

For example, in Python, if the step is storeTitle(variable='foo'), the string for it is ${{variable}} = wd.title. The {variable} is looked up in the step, and the result, foo, is supplied to usedVar, which returns foo.

In Java, the string is ${{variable}:String} = wd.getTitle();. If there has already been another step using the variable foo, ${{variable}:String} is turned into foo. Otherwise, it's turned into String foo.

This means that the first time foo is used we get String foo = wd.getTitle();, and the second time we get foo = wd.getTitle();.

Variables in parameters

Having stored a value in a variable, it may then be referenced in parameter values using the ${varname} syntax. This must be handled by escapeValue. For example, in Python, Bob ${lastname} has to be turned into "Bob " + lastname. Or in PHP, it has to be turned into "Bob " . $lastname.

This process is handled by some transducer code which can be adapted to the needs of the particular language you are writing an exporter for.

Overview of substitution syntax

  • Parameters: {paramname}
  • Locator values: {paramname}
  • Locator type code fragments: {paramnameBy}
  • Variables: ${{varName}:varType}

Within escapeValue:

  • Variables: ${varName}

More information

It's heavily recommended you read the code of some of the exporters. It can also be worthwhile just duplicating an exporter and replacing its parts piecemeal.

If you have any queries, don't hesitate to contact David Stark.

Deploying exporters

We would love more exporters for more languages! If you've written one, send us an email, or submit a pull request on GitHub, and we'll incorporate the exporter into Builder.

Alternatively, you can also create a small plugin that installs your custom exporter.