Why testing Ext JS with Selenium and similar software is nearly impossible?
As I explained in previous note, automatic testing any application is possible provided one is able to write appropriate client, that will interact with the app’s interface in the same manner as regular user does. So… If my application is accessed through browser, then Selenium will suffice, right? It can query DOM tree and invoke actions on found elements. after all. Well, that’s not enough when it comes to testing Ext JS apps.
Why is so? First of all, design issues (or features if you like). Ext JS is a library containing many various components to be used together. Developer writes applications entirely in JavaScript (or ext-js-javascript dialect, as my former colleague used to say), putting provided building blocks together. You need a prompt asking user to enter his e-mail address? Here you are – create a window with panel (container), text field and button for confirmation. In this case, at least four separate components are involved – Window, Panel, Button and TextField. Working example here.
As one could already notice, developer writes no HTML at all. Framework itself cares about markup generation and components appearance uniformity in all supported browsers. But the problem is that resultant HTML is… terrible, at best. Just take a look and try guess what’s happening here:
<div class="x-window x-layer x-window-default x-closable x-window-closable x-window-default-closable x-border-box x-resizable x-window-resizable x-window-default-resizable" role="dialog" aria-hidden="false" aria-disabled="false" aria-labelledby="window-1009_header-title-textEl" id="window-1009" tabindex="-1" data-componentid="window-1009" style="z-index: 19000; width: 299px; height: 140px; right: auto; left: 199px; top: 276px;"> <span id="window-1009-tabGuardBeforeEl" data-ref="tabGuardBeforeEl" aria-hidden="true" class="x-tab-guard x-tab-guard-" style="width:0px;height:0px;" tabindex="0"></span> <div class="x-window-header x-header x-header-draggable x-docked x-unselectable x-window-header-default x-horizontal x-window-header-horizontal x-window-header-default-horizontal x-top x-window-header-top x-window-header-default-top x-box-layout-ct" role="presentation" id="window-1009_header" style="right: auto; left: -2px; top: -2px; width: 299px;"> <span id="window-1009_header-tabGuardBeforeEl" data-ref="tabGuardBeforeEl" aria-hidden="true" class="x-tab-guard x-tab-guard-" style="width:0px;height:0px;"></span> <div id="window-1009_header-innerCt" data-ref="innerCt" role="presentation" class="x-box-inner" style="width: 267px; height: 20px;"> <div id="window-1009_header-targetEl" data-ref="targetEl" class="x-box-target" role="presentation" style="width: 267px;"> <div class="x-title x-window-header-title x-window-header-title-default x-box-item x-title-default x-title-rotate-none x-title-align-left" role="presentation" unselectable="on" id="window-1009_header-title" style="right: auto; top: 0px; margin: 0px; left: 0px; width: 245px;"> <div id="window-1009_header-title-textEl" data-ref="textEl" class="x-title-text x-title-text-default x-title-item" unselectable="on" role="presentation">Prompt demonstration</div> </div> <div class="x-tool x-box-item x-tool-default x-tool-after-title" role="presentation" id="tool-1013" data-qtip="Close dialog" style="right: auto; top: 2px; margin: 0px; left: 251px;"> <div id="tool-1013-toolEl" data-ref="toolEl" class="x-tool-tool-el x-tool-img x-tool-close " role="presentation"></div> </div> </div> </div> <span id="window-1009_header-tabGuardAfterEl" data-ref="tabGuardAfterEl" aria-hidden="true" class="x-tab-guard x-tab-guard-" style="width:0px;height:0px;"></span></div> <div id="window-1009-bodyWrap" data-ref="bodyWrap" class="x-window-bodyWrap" role="presentation"> <div id="window-1009-body" data-ref="body" class="x-window-body x-window-body-default x-closable x-window-body-closable x-window-body-default-closable x-window-body-default x-window-body-default-closable x-noborder-trbl x-resizable x-window-body-resizable x-window-body-default-resizable" role="presentation" style="left: 0px; top: 42px; width: 295px; height: 94px;"> <div id="window-1009-outerCt" data-ref="outerCt" class="x-autocontainer-outerCt" role="presentation"> <div id="window-1009-innerCt" data-ref="innerCt" style="" role="presentation" class="x-autocontainer-innerCt"> <div class="x-panel x-window-item x-panel-default" style="padding: 10px; width: 295px; height: 94px;" role="presentation" id="panel-1010"> <div id="panel-1010-bodyWrap" data-ref="bodyWrap" class="x-panel-bodyWrap" role="presentation"> <div id="panel-1010-body" data-ref="body" class="x-panel-body x-panel-body-default x-panel-body-default x-noborder-trbl" role="presentation" style="width: 275px; height: 74px; left: 0px; top: 0px;"> <div id="panel-1010-outerCt" data-ref="outerCt" class="x-autocontainer-outerCt" role="presentation"> <div id="panel-1010-innerCt" data-ref="innerCt" style="" role="presentation" class="x-autocontainer-innerCt"> <div class="x-field x-form-item x-form-item-default x-form-type-text x-field-default x-autocontainer-form-item" role="presentation" id="textfield-1011"> <label id="textfield-1011-labelEl" data-ref="labelEl" class="x-form-item-label x-form-item-label-default x-unselectable" style="padding-right:5px;width:105px;" for="textfield-1011-inputEl"><span class="x-form-item-label-inner x-form-item-label-inner-default" style="width:100px"><span id="textfield-1011-labelTextEl" data-ref="labelTextEl" class="x-form-item-label-text">Enter name:</span></span> </label> <div id="textfield-1011-bodyEl" data-ref="bodyEl" role="presentation" class="x-form-item-body x-form-item-body-default x-form-text-field-body x-form-text-field-body-default "> <div id="textfield-1011-triggerWrap" data-ref="triggerWrap" role="presentation" class="x-form-trigger-wrap x-form-trigger-wrap-default"> <div id="textfield-1011-inputWrap" data-ref="inputWrap" role="presentation" class="x-form-text-wrap x-form-text-wrap-default"> <input id="textfield-1011-inputEl" data-ref="inputEl" type="text" size="1" name="username" aria-hidden="false" aria-disabled="false" role="textbox" aria-invalid="false" aria-readonly="false" aria-describedby="textfield-1011-ariaStatusEl" aria-required="false" class="x-form-field x-form-text x-form-text-default " autocomplete="off" data-componentid="textfield-1011"> </div> </div> <span id="textfield-1011-ariaStatusEl" data-ref="ariaStatusEl" aria-hidden="true" class="x-hidden-offsets"></span><span id="textfield-1011-ariaErrorEl" data-ref="ariaErrorEl" aria-hidden="true" aria-live="assertive" class="x-hidden-clip"></span></div> </div> <a class="x-btn x-unselectable x-btn-default-small" hidefocus="on" unselectable="on" role="button" aria-hidden="false" aria-disabled="false" id="button-1012" tabindex="0" data-componentid="button-1012"><span id="button-1012-btnWrap" data-ref="btnWrap" role="presentation" unselectable="on" style="" class="x-btn-wrap x-btn-wrap-default-small "><span id="button-1012-btnEl" data-ref="btnEl" role="presentation" unselectable="on" style="" class="x-btn-button x-btn-button-default-small x-btn-text x-btn-button-center "><span id="button-1012-btnIconEl" data-ref="btnIconEl" role="presentation" unselectable="on" class="x-btn-icon-el x-btn-icon-el-default-small " style=""></span><span id="button-1012-btnInnerEl" data-ref="btnInnerEl" unselectable="on" class="x-btn-inner x-btn-inner-default-small">Send</span></span></span></a></div> </div> </div> </div> </div> </div> </div> </div> </div> <span id="window-1009-tabGuardAfterEl" data-ref="tabGuardAfterEl" aria-hidden="true" class="x-tab-guard x-tab-guard-" style="width:0px;height:0px;" tabindex="0"></span> <div id="window-1009-north-handle" class="x-resizable-handle x-resizable-handle-north x-window-handle x-window-handle-north x-window-handle-north-br x-unselectable" unselectable="on" role="presentation" data-exttouchaction="13"></div> <div id="window-1009-south-handle" class="x-resizable-handle x-resizable-handle-south x-window-handle x-window-handle-south x-window-handle-south-br x-unselectable" unselectable="on" role="presentation" data-exttouchaction="13"></div> <div id="window-1009-east-handle" class="x-resizable-handle x-resizable-handle-east x-window-handle x-window-handle-east x-window-handle-east-br x-unselectable" unselectable="on" role="presentation" data-exttouchaction="14"></div> <div id="window-1009-west-handle" class="x-resizable-handle x-resizable-handle-west x-window-handle x-window-handle-west x-window-handle-west-br x-unselectable" unselectable="on" role="presentation" data-exttouchaction="14"></div> <div id="window-1009-northeast-handle" class="x-resizable-handle x-resizable-handle-northeast x-window-handle x-window-handle-northeast x-window-handle-northeast-br x-unselectable" unselectable="on" role="presentation" data-exttouchaction="12"></div> <div id="window-1009-northwest-handle" class="x-resizable-handle x-resizable-handle-northwest x-window-handle x-window-handle-northwest x-window-handle-northwest-br x-unselectable" unselectable="on" role="presentation" data-exttouchaction="12"></div> <div id="window-1009-southeast-handle" class="x-resizable-handle x-resizable-handle-southeast x-window-handle x-window-handle-southeast x-window-handle-southeast-br x-unselectable" unselectable="on" role="presentation" data-exttouchaction="12"></div> <div id="window-1009-southwest-handle" class="x-resizable-handle x-resizable-handle-southwest x-window-handle x-window-handle-southwest x-window-handle-southwest-br x-unselectable" unselectable="on" role="presentation" data-exttouchaction="12"></div> </div>
…and it all for just a simple prompt! Querying such tag soup for interesting DOM element to interact with is next to impossible. Trying to do so will certainly give headache to any daredevil.
So what’s the alternative, beside throwing away Ext JS? Actually, solution is quite simple. It amounts to using builtin components selector and then firing desired events on them. For example, looking for the button from previous example and clicking it can be written down like this:
var allButtonsInWindow = Ext.ComponentQuery.query('button[text=Send]'); allButtonsInWindow[0].click();
In this example I found and clicked button that has an attribute text with value Send. Ext.ComponentQuery.query is very powerful. Although it resembles CSS selectors, ComponentQuery is poorer, but still sufficient for most cases. It brings whole thing to a higher abstraction level. Of course we still do queries, but not for DOM elements and attributes, only components. The query part looks as follows:
button[text=Send]
button is not an HTML tag name here. It is an xtype – “class” of component.
text=Send is not an innerText query of some kind. It only means text attribute of component equal to Send.
Similarly, we could query for the field expecting username (xtype textfield)
var fields = Ext.ComponentQuery.query('textfield[fieldLabel=Enter name]'); fields[0].setValue('Bazinga');
This technique becomes even more powerful, when developers create their own components extending existing ones. Usually you would assign new xtypes yourself, which enables more precise queries.
So testing Ext JS doesn’t have to be a nightmare. It just requires understanding what is Ext JS made of and writing some glue code between testing tools and library itself.
Ext JS Pathfinder goals
- Provide javascript library that will simplify interacting with as many components in Ext JS as possible
- Support for Ext JS 4, 5 and 6
- Seamless integration with other javascript testing libraries, especially those which can open browser window, for example webdriver.io