RHQ GUI Testing: SmartGWT and SeleniumHQSeleniumHQ automation is our strategy for workflow testing of the SmartGWT implemented CoreGUI. Minimum Versions Needed (and those used as of this writing)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.
Setup
SmartGWT uses custom locators to identify the GUI widgets.The scLocators and more is described in the doc located in your <smartgwt-install>/selenium/user-guide.html. Read through this. SmartGWT provides its own user extensionsSelenium allows for javascript extensions (customizations). SmartGWT Selenium integration provides required extensions in its download (user-extensions.js and user-extensions-ide.js). The extensions files must be declared in the Selenium IDE options as described in the Selenium docs or the SmartGWT docs in <smartgwt-install>/selenium/user-guide.html. The setup steps mentioned earlier had you install these into the Selenium IDE options. Extensions for Conditional LogicSelenium 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. General ApproachStarting 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. Assigning Explicit IDsThe following should be true of the explicit IDs we assign:
SmartGWT WidgetsEvery 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. GWT UIObject or HTMLThings 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. Selenium Hook and Utility ClassesMoving 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:
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. Adding Hooks to the CodeThere two basic approaches to getting the hooks in place:
Specifying the locatorId
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. The Acid TestTo check if your Locatable class is truly locatable do two things:
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. Tips
Generating ScriptsTo 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. Starting RHQ GUI with Selenium Hooks EnabledTo 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: enableLocators=true|false 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. Script TipsThings that will help your scripts run. No default IDsIf 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. Right Click in the UIWhen 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. Top Menu Item locatorsYou will need to replace the click commands generated for Top Menu interactions (e.g. Inventory, Administration, Log Out).
Use WaitForVisible commandIt 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:
Here is an example of waiting for a button to be visible before clicking it: waitForVisible scLocator=//IButton[ID="LocatableIButton_Inventory_Mixed_New"]/ click scLocator=//IButton[ID="LocatableIButton_Inventory_Mixed_New"]/ Also, WaitForPageLoad can be used for a straight timeout, setting the Target to the desired value, in milliseconds. Avoid RHQ-Id matching for List View locatingLocators 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:
scLocator=//ListGrid[ID="ListGrid-ResourceGroups"]/body/row[id=10127||name=My%20Test%20Group||8]/col[fieldName=name||1]
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: id (shorter) : scLocator=//ListGrid[ID="ListGrid-ResourceGroups"]/body/row[id=10127]/col[fieldName=name||1] name : scLocator=//ListGrid[ID="ListGrid-ResourceGroups"]/body/row[name=My%20Test%20Group]/col[fieldName=name||1] row : scLocator=//ListGrid[ID="ListGrid-ResourceGroups"]/body/row[8]/col[fieldName=name||1] Trouble SpotsThis 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. Error Message: ID Conflict resolved: 'someID'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. It is also important to remember that the Java being written for GWT execution is generating a DOM backing the Javascript. This DOM is not subject to Java garbage collection so just because something goes out of scope it may not be out of the DOM. It may be that explicit detroy() calls will be necessary. 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. IPickTreeItem (unresolved)This widget seems to utilize javascript implemented sub-widgets. For example, SelectionTreeMenu will show up in the resulting scLocator when making a selection from the picker's tree. I don't think we can get a handle on this widget to assign an explicit ID. Therefore it seems like automation scripts will need to avoid including this widget type. I'm not sure if there is an alternative. 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. TopMenu (resolved)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. File Upload (unresolved)Not sure yet how to incorporate a file upload into a script.
Sample ScriptsLogin and LogoutA short test that logs in and logs out. It can be run repeatedly. public class login-logout extends SeleneseTestNgHelper { @Test public void testLogin-logout() throws Exception { selenium.open("/coregui/CoreGUI.html"); selenium.type("scLocator=//Window[ID=\"isc_Window_0\"]/item[0][Class=\"DynamicForm\"]" + "/item[name=user||title=User||value=rhqadmin||index=2||Class=TextItem]/element", "rhqadmin"); selenium.type("scLocator=//Window[ID=\"isc_Window_0\"]/item[0][Class=\"DynamicForm\"]" + "/item[name=password||title=Password||value=rhqadmin||index=3||Class=PasswordItem]/element", "rhqadmin"); selenium.click("scLocator=//Window[ID=\"isc_Window_0\"]/item[0][Class=\"DynamicForm\"]" + "/item[name=login||title=Login||index=4||Class=SubmitItem]/canvas/"); selenium.waitForPageToLoad("2000"); selenium.clickAt("Identifier=Log Out", ""); } } Platform Group CreateA 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.
Notes:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head profile="http://selenium-ide.openqa.org/profiles/test-case"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <link rel="selenium.base" href="" /> <title>test-group-create</title> </head> <body> <table cellpadding="1" cellspacing="1" border="1"> <thead> <tr><td rowspan="1" colspan="3">test-group-create</td></tr> </thead><tbody> <tr> <td>clickAt</td> <td>Identifier=Inventory</td> <td></td> </tr> <tr> <td>waitForVisible</td> <td>scLocator=//TreeGrid[ID="LocatableTreeGrid_Groups"]/body/row[3]/col[fieldName=nodeTitle||0]</td> <td></td> </tr> <tr> <td>click</td> <td>scLocator=//TreeGrid[ID="LocatableTreeGrid_Groups"]/body/row[3]/col[fieldName=nodeTitle||0]</td> <td></td> </tr> <tr> <td>waitForVisible</td> <td>scLocator=//IButton[ID="LocatableIButton_Inventory_Mixed_New"]/</td> <td></td> </tr> <tr> <td>storeElementPresent</td> <td>scLocator=//ListGrid[ID="LocatableListGrid_Inventory_Mixed"]/body/row[name=TestGroup||0]/col[fieldName=name||1]</td> <td>testGroupExists</td> </tr> <tr> <td>gotoIf</td> <td>storedVars['testGroupExists']==false</td> <td>target1</td> </tr> <tr> <td>click</td> <td>scLocator=//ListGrid[ID="LocatableListGrid_Inventory_Mixed"]/body/row[name=TestGroup||0]/col[fieldName=name||1]</td> <td></td> </tr> <tr> <td>click</td> <td>scLocator=//IButton[ID="LocatableIButton_Inventory_Mixed_Delete"]/</td> <td></td> </tr> <tr> <td>click</td> <td>scLocator=//Dialog[ID="isc_globalWarn"]/yesButton/</td> <td></td> </tr> <tr> <td>label</td> <td>target1</td> <td></td> </tr> <tr> <td>waitForVisible</td> <td>scLocator=//IButton[ID="LocatableIButton_Inventory_Mixed_New"]/</td> <td></td> </tr> <tr> <td>click</td> <td>scLocator=//IButton[ID="LocatableIButton_Inventory_Mixed_New"]/</td> <td></td> </tr> <tr> <td>type</td> <td>scLocator=//DynamicForm[ID="LocatableDynamicForm_GroupCreate"] /item[name=name||title=Name||value=TestGroup||index=0||Class=TextItem]/element</td> <td>TestGroup</td> </tr> <tr> <td>click</td> <td>scLocator=//DynamicForm[ID="RadioGroupWithComponentsItem$RGWCCanvas_groupType"] /item[name=Mixed||title=Mixed||index=0||Class=RadioGroupItem] /item[name=%24540Mixed||title=Mixed||index=0||Class=RadioItem]/element</td> <td></td> </tr> <tr> <td>type</td> <td>scLocator=//DynamicForm[ID="LocatableDynamicForm_GroupCreate"] /item[name=description||title=Description||value=Test%20Group||index=1||Class=AutoFitTextAreaItem]/element</td> <td>Test Group</td> </tr> <tr> <td>click</td> <td>scLocator=//IButton[ID="LocatableIButton_Next"]/</td> <td></td> </tr> <tr> <td>waitForVisible</td> <td>scLocator=//ListGrid[ID="LocatableListGrid_SelectMembers_availableGrid"]/body/row[0]/col[fieldName=name||1]</td> <td></td> </tr> <tr> <td>click</td> <td>scLocator=//ListGrid[ID="LocatableListGrid_SelectMembers_availableGrid"]/body/row[0]/col[fieldName=name||1]</td> <td></td> </tr> <tr> <td>click</td> <td>scLocator=//ImgButton[ID="LocatableTransferImgButton_SelectMembers_RIGHT"]/</td> <td></td> </tr> <tr> <td>click</td> <td>scLocator=//IButton[ID="LocatableIButton_Next"]/</td> <td></td> </tr> </tbody></table> </body> </html> |
Comments (2)
Aug 13, 2010
Jeff Weiss says:
The locators in the sample code above are a different format than the XPath loca...The locators in the sample code above are a different format than the XPath locators we're accustomed to using. But the same concept probably applies - you shouldn't specify the whole path structure down to the element you need, only enough of the branch (or leaf) to uniquely identify the object. The same goes for attributes, you should only specify enough of them to locate the object, and no more.
For instance, all you should need for the username input is name=user and Class=TextItem. Does "//item[name=user||Class=TextItem]/element" work? It should not matter where the input is on the page. Having locators like this make the automation less prone to breakage if the structure of the page should change later (and it often does).
Aug 19, 2010
John Sanda says:
The names we choose for test classes and test methods is important as it can/sho...The names we choose for test classes and test methods is important as it can/should document what is being tested. In one of the examples above we have a class, login-logout with a test method, testLogin-logout. The class name tells me that it is exercising login/logout behavior, but the test method does not convey any additional information. Consider the following test method names,
These method names clearly communicate what behavior they are exercising. Self-documenting names are really helpful (among other times) when reviewing reports or when reviewing the tests themselves to see what functionality is being tested.