Interface UsingSelenium
Using Selenium Scripts (Selenese)
Selenium is a powerful and popular tool which can be used to test your Smart GWT applications. For an overview of Automated
Testing in Smart GWT, see the documentation here
.
Selenium executes tests against your running application in a browser emulating user interaction and asserting various conditions. Selenium provides a record/playback tool for authoring tests without learning a test scripting language - we refer to these as Selenium scripts or Selenese. You must be familiar with Selenium and use of Selenium IDE before proceeding. Refer to the documentation on the Selenium site.
Note that Selenium IDE 3 for Firefox Quantum has been released, but only recently added plugin APIs that should eventually allow us to add support for our our custom locators and commands (replacing our JavaScript user extensions from Selenium IDE 2.9). For now, if you want to write Selenese as this section describes, you must use Selenium IDE 2.9, which can be installed in Firefox 52 ESR, the extended-support release of Firefox which is still receiving updates at the time of this writing.
Selenium supports the concept of Locators in order to specify the element you'd like a given Selenium command to target. For example Selenium supports XPath based locators and DOM ID based locators. XPath based locators are extremely fragile due to complexity of certain highly nested DOM elements you need access to combined with the fact that XPath support varies across browsers and so your tests might not work across different browsers.
Use of Selenium with Smart GWT applications is no different than using Selenium to write and run test cases with any other application with the exception of one caveat: Smart GWT occasionally renders a different DOM structure depending on the browser for performance or rendering the UI such that it appears identical across various browsers. As a result using DOM ID or DOM XPath based locators with Smart GWT applications is not advisable.
Instead Smart GWT supports a new Selenium locator which is an XPath-like string used by Selenium to robustly identify DOM elements within a Smart GWT application. Smart GWT locators for Selenium are prefixed by "scLocator=" and have a readable XPath-like value even for cells in ListGrid's or TreeGrids. Typically these locators will not be hand-written and are generated by Selenium IDE, Selenium's test recording tool. One primary locator is based on the ID of the Smart GWT widget and has the syntax ID=<Canvas ID>. This simplifies the task of writing tests if you know the ID of the Canvas. For reference, the scLocator syntax for ListGrid cells and DynamicForm FormItems can be found at the end of this document.
You can automate the process of
running Selenium tests and saving or reporting results using TestRunner
.
Setup Instructions
Smart GWT ships with two Selenium user extension Javascript files:
- user-extensions.js
- user-entensions-ide.js
These extensions (found in the selenium/
directory) augment the Selenium tools to support Smart GWT locators. To integrate these
extensions with Selenium, follow the steps below:
- Confirm that the Selenium IDE has been installed.
- Copy the user extension files listed above to a common location on your test client machine.
- Open the Selenium IDE and click the Options ==> Options... menu item. On the General tab enter the path to these extension files in the corresponding fields: Selenium Core extensions and Selenium IDE extensions. Refer to the Selenium Documention on user extensions for more information.
- Go to the WebDriver tab and ensure that the "Enable WebDriver Playback" checkbox is unchecked.
- Close and restart Selenium IDE to load the new extensions.
That's it, we're done configuring the environment.
Note: Tests recorded using Selenium IDE can be played back programmatically (e.g. from a test harness) using SeleneseRunner, a simulation tool that executes Selenese against Smart GWT's WebDriver wrappers. This is required because Selenium 3 no longer supports executing Selenese using custom user extensions, as it no longer contains much of the Selenium RC code base.
Recording Selenium tests with Selenium IDE
Once you have your application running in Firefox, open Selenium IDE from the Tools ==> Selenium IDE menu option. If the Selenium IDE is in record mode, then clicking or carrying out other operations like typing in a text field with automatically record the appropriate Selenium commands with the Smart GWT locator. In most cases there's no need for you to manually enter the locator, the recorder does this for you! In fact, not only do the provided user extension files record your clicks, drag operations, and typing in the browser--they also try to ensure that your script executes each operation only when the Smart GWT widgets it depends upon exist and are ready to be interacted with. This ensures that when the test script is executed, then even if one or more triggered operations are asynchronous (delayed), it behaves as expected.
In the screenshot below, note the waitForElementClickable() operation above the click operation; it was added automatically by our user extensions as the click itself was recorded:
Sometimes users may also want finer grain control of what Selenium command is created instead of having the Selenium IDE recorder do this automatically. For example if you want to verify the value of a particular cell in a ListGrid. Instead of typing in the command "verifyTable" and manually enter the Smart GWT Locator (scLocator), you can simply right click on the table cell or any other Smart GWT widget and the most suitable Selenium commands will appear in the context menu along with the scLocator path for the clicked element. See image below.
Solving Ordering Issues in Selenium Scripts
Fundamentally, the reason we add waitForElementClickable() calls before each click is to deal with asynchronous Smart GWT operations. Many operations on widgets or the network are asynchronous, and a correctly coded test should wait for such operations to complete as opposed to inserting an arbitrary delay or using Selenium's setSpeed() function. Using such delays runs the risk of the test failing if replay occurs on a loaded machine or slow network, and also makes the test run slower than needed.
Asynchronous operations include:
- any actual network operation,
- any DataSource operation (even for a clientOnly DataSource),
- any situation where a widget can be marked "dirty" (see notes at Canvas.isDirty()), and then asynchronously redraw itself - this includes API calls like ListGrid.setData(), Canvas.setContents() as well as user interactions like ListGrid sort or filter, regardless of whether the data is already present,
- re-layout that occurs as a result of a size change or new member being added to a Layout or subclass of Layout (eg SectionStack, Window)
The following operations are synchronous and don't require waiting:
- draw()ing any widget that has no parent - but note adding a widget to an already-drawn Layout is asynchronous, as above
You may encounter cases where you have to manually insert a waitForElementClickable() or waitForElementNotPresent() to get a script to behave properly. Looking at the Smart GWT Showcase Example (Grids / Filtering / Advanced Filter), suppose we wanted to filter by country names containing "Za" and wait for the filter step to complete before proceeding. Since the ListGrid initially contains many entries and Zaire is not among them, it is not visible and thus we can solve the original problem by manually adding a waitForElementClickable() on the locator for Zaire's ListGrid entry:
scLocator=//ListGrid[ID="filterGrid"]/body/row[pk=216||countryCode=CG||215]/col[fieldName=countryCode||0]
Before the filter operation is issued, the locator is not clickable because the record is not visible:
When the filter operation completes, Zaire and the other search results become visible and the waitForElementClickable() returns successfully allowing the next script command to execute:
Finally, suppose you wanted to do another filter operation to look only at countries (from the previous search results) with populations under 30 million. Since Zaire is above this limit, it will be missing from the search results and you could wait for the filter operation to complete by adding a waitForElementNotPresent() on same locator that we previously used for waitForElementClickable(). It will return true and allow the script to proceed when the filter operation completes:
Waiting on Pending ListGrid Operations
There are cases where waitForElementClickable()/waitForElementNotPresent() will not work--for example if you're performing a sort that's rearranging existing elements on the screen, or if you're performing a filter operation where you're not sure of the results and thus cannot use the approach from the previous section. In such a situation, you may need to add a waitForGridDone() command into your script to ensure the pending operations are complete before you hit the next command.
The waitForGridDone() command guarantees it will not complete successfully unless all of the following potential pending operations on the widget are complete:
- any fetch or filter operation (the result of applying criteria),
- any sort operation (the result of apply sort specifiers),
- the flush of pending FilterEditor criteria to the parent ListGrid, and
- the saving of any newly edited rows.
This command should be able to block a Selenium script until the ListGrid specified in the locator reaches a stable drawn state with no pending activity. So for a ListGrid names 'filterGrid', all you'd need to add to ensure all pending operations on it have completed is the command:
waitForGridDone("//ListGrid[ID='filterGrid']");
Waiting on All Pending Network Operations
Because of the waitForElementClickable commands which are automatically inserted during recording, your scripts will automatically wait for the completion of any network operations that block interactivity (via showPrompt, which is enabled by default). However in some cases you may want to wait for all pending network operations to complete, even if they don't block user interactivity.
To do this, use RPCManager.requestsArePending() in combination with waitForCondition(). So, the JavaScript in your waitForCondition() operation would be:
!selenium.browserbot.getCurrentWindow().isc.RPCManager.requestsArePending()
When the call returns, you'd know that any previously initiated network operations--such as filter/sort operations on DataSources--are complete.
Automatically Waiting on All Pending Network Operations
If you need the functionality from the section above to wait on all pending network operations, but don't want to add extra calls to waitForCondition(), you may switch on automatic enforcement of the condition that isc.RPCManager.requestsArePending() is false. There are two ways to do this:
- Set the property isc.AutoTest.implicitNetworkWait to true on the page under test after the ISC modules are loaded, or
- Add the Selenium command setImplicitNetworkWait(true) to your selenium script in Selenium IDE.
Like other Selenium IDE commands with a single argument, you'll want to use setImplicitNetworkWait() by passing true (or false) in the Target field of the Selenium IDE GUI (right under command). Without any modifications, the default value for isc.AutoTest.implicitNetworkWait of false will prevail.
Keystroke Capturing
Our Selenium Extensions will automatically record the following type of keyboard activity in Smart GWT widgets, on a keystroke-by-keystroke basis:
- typing at widgets other than text items (e.g. ListGrids), including normal printing characters, navigation keys (up, right, etc.), or modifier sequences (e.g. Ctrl-V)
- typing of printing characters in masked text items
auto child
of another form item (e.g. MultiComboBoxItem
). In this case, you can tap Alt
(Option on Mac) to manually insert a "type" command with the right value. Recording Movement-Driven Interactions
Our Selenium Extensions provide the capability to automatically record click-based interactions with the target page, and keystrokes (under certain circumstances). However, we don't automatically capture interactions based solely on movement, such as when a nested menu flyout is triggered by moving the mouse over the menu item of an existing menu. To avoid problems:
- If you're trying to record interactions with menus or submenus of a ListGrid, the recommended approach is to use a right mouse click (i.e. "context menu" command) interaction to record the opening of the initial menu, rather than clicking on the HeaderMenuButton that's shown on the header buttons. For the submenus, it's recommended to click on the appropriate (parent) menu item to properly record what's needed, even though the child menu is already visible from your mouse movement.
- To manually record a "mouse move" operation over the current element, you can simply press the Alt key (Option on Mac). (See also "KeyStroke Capturing" above for behavior over text items.)
- If you find yourself often having to add commands manually (other than through the above mechanism), you may be approaching the situation incorrectly. In that case check the forums to see if it's a common problem.
Capturing Logs
Capturing of client and server-side logs can be
switched on by providing appropriate options to TestRunner
,
but a few Selenium commands are provided to provide direct control over logging on a per-script
basis. If server logging has been configured as "some," then server logs won't be captured for
a given script unless you add the captureServerLogs() Selenium command after the open command;
switching the mode to "all" will force server logs to be collected for all Selenium scripts,
and no captureServerLogs() command is then required.
To configure logging levels, you can use the commands setClientLogLevel(category, level), or setServerLogLevel(category, level). For example:
- setClientLogLevel("AutoTest","ERROR"), or
- setServerLogLevel("com.isomorphic.rpc.RPCManager", "INFO")
Disabling the Selenium Smart GWT URL Query String
By default, our user extensions automatically add a special URL variable, sc_selenium, to open command urls to allow JavaScript to detect it's being driven by Selenium in case special logic should be used. In the unlikely event that this causes a problem with your code or page loading and you don't need the feature, you may eliminate this special URL variable by changing Selenium.prototype.use_url_query_sc_selenium from true to false in user-extensions.js.
Common scLocator syntax
For more information
on how locators are formed and how to influence them, see the AutoTest
class in the Smart GWT JavaDoc.
List Grid cells
//ListGrid[ID="itemList"]/body/row[itemID=1996||itemName=Sugar White 1KG||SKU=85201400||1]/col[fieldName=SKU||1]
- This assumes the ListGrid has an explicit ID
- the 'body' part might be 'frozenBody' if the field in question was frozen
- row[......] identifies the row (record)
- itemID= - that's the primary key field from the dataSource the grid is bound to
- itemName= - that's the title field value for the record
- SKU=... - that's the cell the user clicked's value
- 1 - that's the index of the row (rowNum)
- col[.....] - identifies the column in the grid
- fieldName=... - field name for the field the user clicked
- 1 - that's the index of the column
Form Items
//DynamicForm[ID="autoTestForm"]/item[name=textField||title=textField||value=test||index=0||Class=TextItem]/element
This example is the data element (text entry box) for a text field
- this form has an explicit ID
- item[...] identifies the item
- name (field name, if set)
- title (title, if set)
- value (current value if set)
- index (index in the form items array)
- Class (SC class of the item - in this case TextItem) after the "/" we identify the part of the item in question options here include:
- "element" - the data element
- "canvas" - for CanvasItems - points to the canvas embedded in the item
- in this case the xpath might continue to contain, for example children of the canvas or elements within it (cells in a listGrid, etc)
- "textbox" - the "text box" - this is the area where content is written out for items without a 'data element' - like header items
- "[icon=<...>]" - the icon element -- "<...>" would contain the "name" of the icon
Special scLocator usage
Verifying icon/image loading
If you manually add
/imageLoaded to the end of the locator generated for a Button
or Img
, then AutoTest.getElement()
or AutoTest.getValue()
can be used to verify whether
the icon (in the case of a Button
) or image (for an Img
) have been
loaded succesfully.
Examples:
- //Button[ID="cssButton"]/imageLoaded
- //IButton[ID="stretchButton"]/imageLoaded
- //ImgButton[ID="imgButton"]/imageLoaded
- //Img[ID="photo"]/imageLoaded
Verifying DrawItem attributes
If you record a locator to a DrawItem
, by
default its value will be the title
, or the contents
for a
DrawLabel
. However, other attributes, as listed
below, can be queried by adding a trailing suffix. (Those appearing as links call the
associated getter rather than accessing a property directly.) Note that not all properties
listed are available on all DrawItem
s, and that if
multiple values are present, they will be separated by a single space when the Framework passes
them to Selenium.
- src
- rotation
- fillColor, lineColor
- top, left, width, height
- lineWidth, lineOpacity, fillOpacity
center
boundingBox
resizeBoundingBox
Examples:
- //DrawRect[ID="myRect"]/width
- //DrawTriangle[ID="bigTriangle"]/resizeBoundingBox
Best Practices
- Maximize the test browser window to avoid offscreen
widgets: Some browsers will not respond to events on widgets that are not visible in the
browser pane (scrolled out of view or clipped off). To avoid having to manually add script
commands to scroll such widgets into view, it's recommended to use Selenium's
windowMaximize() command which will force the browser to occupy the entire screen.
Note that currently some browsers will respond to events on offscreen widgets (IE will, Firefox will not) however, web standards are unclear on whether this should be allowed and the behavior may change in the future, so best practice is to maximize for all browsers.
-
Use setID() judiciously to ensure stable locators run-to-run: When setID() is not used
to supply a unique component ID, locators will sometimes incorporate automatically generated
IDs which have a sequence number (eg isc_Object_355). If your test has unpredictable execution
order (for example, two simultaneous network operations take place and either may complete
first, and both generate UI components on completion) then these IDs will not be stable from
run-to-run. They will likewise not be stable if you test part of an app and then embed it in a
larger app and try to use the same script.
Use setID() selectively to avoid this problem. Generally, it makes sense to use setID() on all top-level (parentless) widgets - at this point, locators for children that do not have a unique ID will be based on the parent's ID plus a relative path. This relative path will not incorporate auto-generated IDs and will generally continue to work even if the interior layout of the parent is significantly rearranged (such as adding a new intervening container widget).
Known Limitations
- Selenium intermittently fails to generate an scLocator with the "type" command on some FormItems. If this occurs, you can manually enter an scLocator into the target field, or use the drop down to select an alternative locator strategy (such as locating a text input element by name).
- Support for multi-select for SelectItems with selection mode "grid" (non-default)