对于在浏览器中测试 extjs 代码(最好是使用 selenium)有什么建议吗?

我们已经成功地使用了 selenium 来处理高级网站测试(除了在模块级别的大量 python doctest 之外)。然而,现在我们在很多页面中使用 extjs,而且很难将 Selenium 测试集成到网格等复杂组件中。

有人成功地为基于 extjs 的网页编写了自动化测试吗?很多谷歌都能找到有类似问题的人,但是很少有答案。谢谢!

47713 次浏览

Can you provide more insight into the types of problems you're having with extjs testing?

One Selenium extension I find useful is waitForCondition. If your problem seems to be trouble with the Ajax events, you can use waitForCondition to wait for events to happen.

I have been testing my ExtJs web application with selenium. One of the biggest problem was selecting an item in the grid in order to do something with it.

For this, I wrote helper method (in SeleniumExtJsUtils class which is a collection of useful methods for easier interaction with ExtJs):

/**
* Javascript needed to execute in order to select row in the grid
*
* @param gridId Grid id
* @param rowIndex Index of the row to select
* @return Javascript to select row
*/
public static String selectGridRow(String gridId, int rowIndex) {
return "Ext.getCmp('" + gridId + "').getSelectionModel().selectRow(" + rowIndex + ", true)";
}

and when I needed to select a row, I'd just call:

selenium.runScript( SeleniumExtJsUtils.selectGridRow("<myGridId>", 5) );

For this to work I need to set my id on the grid and not let ExtJs generate it's own.

The biggest hurdle in testing ExtJS with Selenium is that ExtJS doesn't render standard HTML elements and the Selenium IDE will naively (and rightfully) generate commands targeted at elements that just act as decor -- superfluous elements that help ExtJS with the whole desktop-look-and-feel. Here are a few tips and tricks that I've gathered while writing automated Selenium test against an ExtJS app.

General Tips

Locating Elements

When generating Selenium test cases by recording user actions with Selenium IDE on Firefox, Selenium will base the recorded actions on the ids of the HTML elements. However, for most clickable elements, ExtJS uses generated ids like "ext-gen-345" which are likely to change on a subsequent visit to the same page, even if no code changes have been made. After recording user actions for a test, there needs to be a manual effort to go through all such actions that depend on generated ids and to replace them. There are two types of replacements that can be made:

Replacing an Id Locator with a CSS or XPath Locator

CSS locators begin with "css=" and XPath locators begin with "//" (the "xpath=" prefix is optional). CSS locators are less verbose and are easier to read and should be preferred over XPath locators. However, there can be cases where XPath locators need to be used because a CSS locator simply can't cut it.

Executing JavaScript

Some elements require more than simple mouse/keyboard interactions due to the complex rendering carried out by ExtJS. For example, a Ext.form.CombBox is not really a <select> element but a text input with a detached drop-down list that's somewhere at the bottom of the document tree. In order to properly simulate a ComboBox selection, it's possible to first simulate a click on the drop-down arrow and then to click on the list that appears. However, locating these elements through CSS or XPath locators can be cumbersome. An alternative is to locate the ComoBox component itself and call methods on it to simulate the selection:

var combo = Ext.getCmp('genderComboBox'); // returns the ComboBox components
combo.setValue('female'); // set the value
combo.fireEvent('select'); // because setValue() doesn't trigger the event

In Selenium the runScript command can be used to perform the above operation in a more concise form:

with (Ext.getCmp('genderComboBox')) { setValue('female'); fireEvent('select'); }

Coping with AJAX and Slow Rendering

Selenium has "*AndWait" flavors for all commands for waiting for page loads when a user action results in page transitions or reloads. However, since AJAX fetches don't involve actual page loads, these commands can't be used for synchronization. The solution is to make use of visual clues like the presence/absence of an AJAX progress indicator or the appearance of rows in a grid, additional components, links etc. For example:

Command: waitForElementNotPresent
Target: css=div:contains('Loading...')

Sometimes an element will appear only after a certain amount of time, depending on how fast ExtJS renders components after a user action results in a view change. Instead of using arbitary delays with the pause command, the ideal method is to wait until the element of interest comes within our grasp. For example, to click on an item after waiting for it to appear:

Command: waitForElementPresent
Target: css=span:contains('Do the funky thing')
Command: click
Target: css=span:contains('Do the funky thing')

Relying on arbitrary pauses is not a good idea since timing differences that result from running the tests in different browsers or on different machines will make the test cases flaky.

Non-clickable Items

Some elements can't be triggered by the click command. It's because the event listener is actually on the container, watching for mouse events on its child elements, that eventually bubble up to the parent. The tab control is one example. To click on the a tab, you have to simulate a mouseDown event at the tab label:

Command: mouseDownAt
Target: css=.x-tab-strip-text:contains('Options')
Value: 0,0

Field Validation

Form fields (Ext.form.* components) that have associated regular expressions or vtypes for validation will trigger validation with a certain delay (see the validationDelay property which is set to 250ms by default), after the user enters text or immediately when the field loses focus -- or blurs (see the validateOnDelay property). In order to trigger field validation after issuing the type Selenium command to enter some text inside a field, you have to do either of the following:

  • Triggering Delayed Validation

    ExtJS fires off the validation delay timer when the field receives keyup events. To trigger this timer, simply issue a dummy keyup event (it doesn't matter which key you use as ExtJS ignores it), followed by a short pause that is longer than the validationDelay:

    Command: keyUp
    Target: someTextArea
    Value: x
    Command: pause
    Target: 500
    
  • Triggering Immediate Validation

    You can inject a blur event into the field to trigger immediate validation:

    Command: runScript
    Target: someComponent.nameTextField.fireEvent("blur")
    

Checking for Validation Results

Following validation, you can check for the presence or absence of an error field:

Command: verifyElementNotPresent
Target: //*[@id="nameTextField"]/../*[@class="x-form-invalid-msg" and not(contains(@style, "display: none"))]


Command: verifyElementPresent
Target: //*[@id="nameTextField"]/../*[@class="x-form-invalid-msg" and not(contains(@style, "display: none"))]

Note that the "display: none" check is necessary because once an error field is shown and then it needs to be hidden, ExtJS will simply hide error field instead of entirely removing it from the DOM tree.

Element-specific Tips

Clicking an Ext.form.Button

  • Command: click Target: css=button:contains('Save')

    Selects the button by its caption

  • Command: click Target: css=#save-options button

    Selects the button by its id

Selecting a Value from an Ext.form.ComboBox

Command: runScript
Target: with (Ext.getCmp('genderComboBox')) { setValue('female'); fireEvent('select'); }

First sets the value and then explicitly fires the select event in case there are observers.

Useful tips for fetch grid via Id of grid on the page: I think you can extend more useful function from this API.

   sub get_grid_row {
my ($browser, $grid, $row)  = @_;




my $script = "var doc = this.browserbot.getCurrentWindow().document;\n" .
"var grid = doc.getElementById('$grid');\n" .
"var table = grid.getElementsByTagName('table');\n" .
"var result = '';\n" .
"var row = 0;\n" .
"for (var i = 0; i < table.length; i++) {\n" .
"   if (table[i].className == 'x-grid3-row-table') {\n".
"       row++;\n" .
"       if (row == $row) {\n" .
"           var cols_len = table[i].rows[0].cells.length;\n" .
"           for (var j = 0; j < cols_len; j++) {\n" .
"               var cell = table[i].rows[0].cells[j];\n" .
"               if (result.length == 0) {\n" .
"                   result = getText(cell);\n" .
"               } else { \n" .
"                   result += '|' + getText(cell);\n" .
"               }\n" .
"           }\n" .
"       }\n" .
"   }\n" .
"}\n" .
"result;\n";


my $result = $browser->get_eval($script);
my @res = split('\|', $result);
return @res;
}

To detect that element is visible you use the clause: not(contains(@style, "display: none")

It's better to use this:

visible_clause = "not(ancestor::*[contains(@style,'display: none')" +
" or contains(@style, 'visibility: hidden') " +
" or contains(@class,'x-hide-display')])"


hidden_clause = "parent::*[contains(@style,'display: none')" +
" or contains(@style, 'visibility: hidden')" +
" or contains(@class,'x-hide-display')]"

For complex UI that is not formal HTML, xPath is always something you can count on, but a little complex when it comes to different UI implementation using ExtJs.

You can use Firebug and Firexpath as firefox extensions to test an certain element's xpath, and simple pass full xpath as parameter to selenium.

For example in java code:

String fullXpath = "xpath=//div[@id='mainDiv']//div[contains(@class,'x-grid-row')]//table/tbody/tr[1]/td[1]//button"


selenium.click(fullXpath);

Easier testing through custom HTML data- attributes

From the Sencha documentation:

An itemId can be used as an alternative way to get a reference to a component when no object reference is available. Instead of using an id with Ext.getCmp, use itemId with Ext.container.Container.getComponent which will retrieve itemId's or id's. Since itemId's are an index to the container's internal MixedCollection, the itemId is scoped locally to the container -- avoiding potential conflicts with Ext.ComponentManager which requires a unique id.

Overriding the Ext.AbstractComponent's onBoxReady method, I set a custom data attribute (whose name comes from my custom testIdAttr property of each component) to the component's itemId value, if it exists. Add the Testing.overrides.AbstractComponent class to your application.js file's requires array.

/**
* Overrides the Ext.AbstracComponent's onBoxReady
* method to add custom data attributes to the
* component's dom structure.
*
* @author Brian Wendt
*/
Ext.define('Testing.overrides.AbstractComponent', {
override: 'Ext.AbstractComponent',




onBoxReady: function () {
var me = this,
el = me.getEl();




if (el && el.dom && me.itemId) {
el.dom.setAttribute(me.testIdAttr || 'data-selenium-id', me.itemId);
}




me.callOverridden(arguments);
}
});

This method provides developers with a way to reuse a descriptive identifier within their code and to have those identifiers available each time the page is rendered. No more searching through non-descriptive, dynamically-generated ids.

Ext JS web pages can be tricky to test, because of the complicated HTML they end up generating like with Ext JS grids.

HTML5 Robot deals with this by using a series of best practices for how to reliably lookup and interact with components based on attributes and conditions which are not dynamic. It then provides shortcuts for doing this with all of the HTML, Ext JS, and Sencha Touch components that you would need to interact with. It comes in 2 flavors:

  1. Java - Familiar Selenium and JUnit based API that has built in web driver support for all modern browsers.
  2. Gwen - A human style language for quickly and easily creating and maintaining browser tests, which comes with its own integrated development environment. All of which is based on the Java API.

For example if you were wanting to find the Ext JS grid row containing the text "Foo", you could do the following in Java:

findExtJsGridRow("Foo");

...and you could do the following in Gwen:

extjsgridrow by text "Foo"

There is a lot of documentation for both Java and Gwen for how to work with Ext JS specific components. The documentation also details the resulting HTML for all of these Ext JS components, which you also may find useful.

This blog helped me a lot. He's written quite a lot on the topic and it seems like its still active. The guy also seems to appreciate good design.

He basically talks about using sending javascript to do queries and using the Ext.ComponentQuery.query method to retrieve stuff in the same way you do in your ext app internally. That way you can use xtypes and itemIds and dont have to worry about trying to parse any of the mad auto-generated stuff.

I found this article in particular very helpful.

Might post something a bit more detailed on here soon - still trying to get my head around how to do this properly

When I was testing ExtJS application using WebDriver I used next approach: I looked for field by label's text and got @for attribute from label. For example, we have a label

<label id="dynamic_id_label" class="TextboxLabel" for="textField_which_I_am_lloking_for">
Name Of Needed Label
<label/>

And we need to point WebDriver some input: //input[@id=(//label[contains(text(),'Name Of Needed Label')]/@for)].

So, it will pick the id from @for attribute and use it further. This is probably the simplest case but it gives you the way to locate element. It is much harder when you have no label but then you need to find some element and write your xpath looking for siblings, descend/ascend elements.

We are developing a testing framework that uses selenium and encountered problems with extjs (since it's client side rendering). I find it useful to look for an element once the DOM is ready.

public static boolean waitUntilDOMIsReady(WebDriver driver) {
def maxSeconds = DEFAULT_WAIT_SECONDS * 10
for (count in 1..maxSeconds) {
Thread.sleep(100)
def ready = isDOMReady(driver);
if (ready) {
break;
}
}
}


public static boolean isDOMReady(WebDriver driver){
return driver.executeScript("return document.readyState");
}