- RHQ GUI Testing: SmartGWT and SeleniumHQ
- General Approach
- Assigning Explicit IDs
- Generating Scripts
- Trouble Spots
- Sample Scripts
SeleniumHQ automation is our strategy for workflow testing of the SmartGWT implemented CoreGUI.
Note that Selenium IDE is useful for experimenting and writing or executing short scripts, but will most likely not be the tool we will use for generating the final set of test scripts. That will more likely be Selenium RC (Remote Control). This will require other downloads and configuration. Details are pending.
|Selenium IDE||1.0.10||Download the FireFox AddOn (via the FF Tools->Add-ons)|
- Download SmartGWT, whose link is found above. Unzip its contents, which we'll call <smartgwt-install>.
- Download the text for this optional yet useful goto_sel_ide.js and put it somewhere on your file system (note: don't just SaveAs that page, you want just the raw .js text, not the HTML page itself). I put it in my selenium directory, where I will also put my selenium downloads.
- Download Selenium IDE and install it into FireFox using the normal FireFox .xsi install mechanism (just asking FireFox to download the .xsi will normally cause it to install, so its pretty easy). Follow the Selenium download link listed above to get this .xsi.
- Restart FireFox to ensure the Selenium IDE is properly installed and started
- Select FireFox's new menu "Tools > Selenium IDE"
- From the Selenium IDE, select its menu option "Options > Options..."
- From the Options dialog, browse to and select "<smartgwt-install>/selenium/user-extension.js" for the Selenium Core Extensions field.
- For that same Selenium Core Extensions field, browse to and select that "goto_sel_ide.js" you downloaded earlier. Clicking OK should add it next to the user-extensions.js file, comma-separated.
- From the Options dialog, browse to and select "<smartgwt-install>/selenium/user-extension-ide.js" for the Selenium IDE Extensions field.
- Click OK and close out the Selenium options and IDE windows. Selenium should be set up now.
The scLocators and more is described in the doc located in your <smartgwt-install>/selenium/user-guide.html. Read through this.
Selenium out of the box does not provide commands for conditional logic. There is a popular set of extensions to provide some level of support for this. Go here to read about, see an example image of using, and download and install goto_sel_ide.js. The setup steps mentioned earlier had you install this into the Selenium IDE. In addition to that page the author has several blog entries with examples. This page is very useful.
Note, Selenium IDE can register several extension files, separated by commas, which we used to install the core smartgwt extensions along with the goto_sel_ide.js.
Starting with SmartGWT 2.2 all of the BaseWidgets are assigned a default ID. The default ID has a format like isc_scClassName_incrementer. For example, isc_IButton_5. Each time a widget for that scClassName is constructed the incrementer is incremented.
The IDs are incorporated into the scLocators. For example, An IButton on say the Resource Groups list View may initially have scLocator=//IButton[ID="isc_IButton_5"]/. Click away and then back to the same page and it may have scLocator=//IButton[ID="isc_IButton_14"]/ or something similar.
The non-deterministic nature of these IDs and therefore the scLocators incorporating them, makes repeatable Selenium scripts almost impossible to generate and/or execute on a complex UI, like ours. To solve this problem we need to assign non-default, predictable IDs to widgets involved in our automation scripts.
The following should be true of the explicit IDs we assign:
- The IDs assigned must be unique among the currently existing Widgets in the DOM (note - even among non-rendered Widgets that have yet to be destroyed).
- The IDs must be predictable, such that the same logical widget on-screen gets the same ID each time.
- for example, a button on a page should have the same ID each time the page is visited
Every SmartGWT widget provides a setID(String ID) method. This method must be called prior to BaseWidget.isCreated() returning true. In general this means calling setID() in the constructor, or no later than onInit(). For example, once onDraw() is called it's too late.
Things are a bit different for a GWT UIObject. In this case the underlying element's "id" attribute must be set. Similarly, for straight HTML the "id" attribute must be set manually on the HTML.
Moving forward we'll be assigning explicit IDs to our CoreGUI widgets. To assist doing this we will use thin wrapper classes around several SmartGWT classes. The wrapper classes will call setID(), generating an ID based on a locatorId passed into its constructor.
As an example, here are some current Wrapper Classes. The convention is to prepend Locatable to the base widget class name. There will be a locatable wrapper class for each Widget class, as we deem necessary:
|SmartGWT class||RHQ wrapper class|
These classes follow a fairly strict format and can be easily copied and edited to create new Locatable wrapper classes.
There is also a SeleniumUtility.java containing static methods for assisting in ID setting. For the most part these methods need not be called directly but only from the wrapper classes. The exception is for GWT UIObject assignment, where SeleniumUtility.setHtmlId() is useful.
Note that these utilities will provide a safeID by removing invalid characters.
There two basic approaches to getting the hooks in place:
- When contructing a widget use the wrapper class as opposed to the base widget class.
- When subclassing, extend the wrapper class as opposed to the base widget class.
- The only time simple literals should be used for a locatableId is if the containing class is itself, not locatable. This is rare, just for the uppermost classes in the hierarchy.
- Each locatable class implements Locatable, which provide the following two methods, which should be used when specifying locatorIds for its subWidgets:
- extendLocatorId( String extension )
The extendLocatorId() method is typically the method to use. This will form a locatorId that concatenates this.getID() and the extension string specified. In this way the new locatorId is qualified by the locatorId of its creating widget and, assuming the extension is not duplicated within the class, should generate unique IDs.
The getLocatorId() method can be used only if you're sure that the underlying widget type for the locatorId is not going to be duplicated. For example, if a LocatableVLayout itself creates only a LocatableTreeGrid then you could feasibly do something like TreeGrid tg = new LocatableTreeGrid( this.getLocatorId() );. Improper use of this call can cause subtle ID conflict issues.
To check if your Locatable class is truly locatable do two things:
- search for "new " in the class and make sure that necessary sub-widgets are also locatable and have sensical locatorIds.
- run the gui with the Selenium IDE active and validate that the scLocators look good.
Note that it is only necessary to make rendered widgets Locatable. It's often the case that we use various Layout widgets for formatting. These themselves are often hidden or not really accessible. These do not have to made Locatable.
- In general, you do not need to create locatables for Item classes (e.g. TextItem). These are already incorporated into the scLocators well.
- Avoid using anything non-deterministic in your locatorId
- anything that may now or in the future be affected by I18N/L10N
- sequence generated DB IDs. These may be ok in very rare circumstances. Ask yourself whether it will likely be the same ID anytime you run a selenium test.
- Be careful of loops generating widgets. In these cases you will need to make sure your locatorId differs on each iteration, but is still repeatable. Sometimes a resource name or some-such may be ok to use.
To generate scripts you can start by using the Selenium IDE FireFox plugin (along with the SmartGwt extensions as described in Setup). This will record your movements through the UI for some use case. The result will most likely not be executable until massaged. See the Tips section. The goal is to have repeatable scripts. It is not useful until it can execute multiple times successfully (although, some set of preconditions may be required, like Inventory, lack of Inventory, various defined objects, etc). To execute repeatedly it can almost definitely involve scLocators that include the default generated Widget IDs, due to the numeric incrementer.
To record scripts you must run the GUI with RHQ generating explicit widget IDs. We call these locatorIds and they replace the default element IDs generating by smartgwt. To do this there is added support for enabling or disabling our selenium locatorIds via the coreGui url. We now look for url param:
If true we enable the use of our explicit locatorIds to be used as selenium hooks. If false, or omitted, we run with default Ids and will not be selenium-ready.
To support this from the maven command line and our eclipse external tools config you can now specify the 'coreGuiParams' property. I've added a new eclipse ext tools config called 'Run GWT DevMode-JPDA-Params' that specifies -D$coreGuiParams=?enableLocators=true.
Things that will help your scripts run.
If your captured script includes default IDs it means that a developer needs to add more hooks (explicit IDs) to the widgets involved. Otherwise the script will not be repeatable due to the non-deterministic incrementers. These will typically look like 'sc_classname_#' where the '#' is some incrementing integer.
When running the Selenium IDE A right click will provide many options that can be added to a script. Minimally it will show you the scLocator for the widget being clicked on, but it will also allow you add many different Assertions or other related commands.
You will need to replace the click commands generated for Top Menu interactions (e.g. Inventory, Administration, Log Out).
- The Top Menu entries needed explicit html identifier locators. They are set like the display text.
- Use ClickAt as opposed to Click for the Top Menu Section
- for example: "clickAt Identifier=Administration"
It is very often the case that a script can't proceed until the necessary widgets have rendered. An approach that seems to work well is to use WaitForVisible, specifying the same scLocator that will then be manipulated (for example, with a subsequent click command). Places this seems necessary:
- after login
- after section Top Menu selection
- after anything that generates a tree
- after most anything that changes the widgets on screen
Here is an example of waiting for a button to be visible before clicking it:
Also, WaitForPageLoad can be used for a straight timeout, setting the Target to the desired value, in milliseconds.
Locators for list entries, by default, include certain column/field values, like "Id" and "Name", as well as a 0-based RowNum. For example, here is an scLocator for a ResourceGroups List Entry:
In particular, note the section [id=10127\|\|name=My%20Test%20Group\|\|8|]. This looks like a logical OR. And maybe that's the intention, but currently the first condition seems to need to match. Perhaps the other tests are there for convenience in editing. So, initially this is looking for id==10127. The problem is that id==10127 may not be true the next time the test is run. 10127 may not be a valid groupId, or it may be assigned to some other group. The scLocator can be edited to remove parts not needed for location. By removing the id==10127 you can still match on the name, or possibly the RowNum. So, to match on the id (not recommended) you can use the default. Or:
This is just an accumulation of known trouble areas.
Error Message: "Cannot change configuration property 'ID' to 'theIdIWantToSet' after the component has been created"
This is typically a popup window.
It's possible the ID actually does not already exist and the call to setID() is simply being performed too late. This may be the case if the ID is not being assigned in a constructor or onInit(). It must be assigned prior to any rendering of the widget.
Bring to the attention of a dev.
This is generated by our SeleniumUtility class and reported in the MessageCenter.
This indicates a widget with the desired ID already exists. This can be tricky to find. It may be that the ID is not unique enough but more likely it is an unexpected existence of a widget with the desired ID.
This can happen in various "leak" situations. For example, setting a break point on the constructor causing the issue may show that it is being invoked from unexpected, or unexpectedly recurring, code-paths. It may mean that additional guards must be put in place to prevent the unexpected, and most likely, undesired executions.
Wizards in particular seem to exacerbate this situation. Even though the Wizard comes and goes, not all of its DOM objects get destroyed in any sort of timely manner, so re-invoking a wizard can be problematic. Although the Wizard framework will initiate a destroy() for its Canvas's, it may be necessary to add onDestroy() hooks to fully cascade the destroy, wiping problematic widgets. See AbstractSelector for an example.
ListGrids with assigned DataSources seem particularly problematic.
To try and resolve the problem the SeleniumUtility will destroy the existing widget in favor of the new widget, and try and continue. It will generate a stack trace to help assist in figuring out why the duplicate is being generated.
Bring to the attention of a dev.
Currently this is the widget used in the Group Create Wizard when selecting Compatible for the group type. It is also used for the type filter when creating a mixed group.
The entries in the Top Menu Bar were not generating useful scLocators. The Section links were all generating a default because they are created with straight html. The other Hyperlinks (e.g. "Log Out") seemed to generate a decent locator but still were not getting picked up.
I've added explicit HTML identifier locators for everything in the Top Menu. The section links set the "id" attribute directly in the HTML snippet. The GWT Hyperlink UIObjects are now wrapped by the appropriate SeleniumUtility method. The IDs are basically the same as the display text.
Not sure yet how to incorporate a file upload into a script.
comment: We may be able to set an option on the widget to allow for manual text entry. by default the native widget immediately invokes the native file selector (e.g. Win Explorer).
comment2: This seems to be a real issue in general, for selenium test generation and file upload. The manual entry, and the setValue() method are disabled for security reasons. Various workarounda are discussed on the web but will take some effort to figure out. Some related links:
A short test that logs in and logs out. It can be run repeatedly.
A longer script that shows a few things. It looks for "TestGroup" and conditionally deletes it before invoking the wizard to create it from scratch. It populates the Mixed group with all of the existing platforms.
- Shows conditional logic branching on whether a list row exists
- Shows an edited scLocator for the list row, eliminating the id an matching on name.
- Shows the need for several waitForVisible commands
- Shows several different widget types and the explicit IDs set by our hooks
- Shows Identifier locator
- Assumes a logged in session with group create permission.
- Some scLocators have been wrapped below for formatting
- Script can be downloaded from this page's attachments