📜 ⬆️ ⬇️

Testing ExtJS / Sencha components and applications using the PhantomJS engine

PhantomJS is a WebKit engine assembly without a graphical interface that allows you to load a web page in console mode, execute JavaScript, fully work with DOM, Canvas and SVG. One of the main applications declared PhantomJS is automated functional testing of the user interface. PhantomJS has integration with various frameworks for testing JavaScript and web pages. Let's see what can be done on the basis of the standard PhantomJS functionality to test a single component and an entire application written in ExtJS / Sencha . In this article, I will provide some simplest template for a testing framework, illustrating the approach to testing code based on a third-party JavaScript library. All the code presented in the article is available on GitHub .



Component testing


')
Consider a simple component that extends the standard Ext.form.ComboBox drop-down list. It should contain a list of months, customizable using the months configuration parameter, an array containing the numbers of the displayed months, starting with zero. For example, if an array consists of the numbers 1, 3, and 5, then the drop-down list should display the months of February, April, and June.

Ext.define('MonthComboBox', { extend : 'Ext.form.ComboBox', alias : 'widget.monthcombo', store : Ext.create('Ext.data.Store', { fields : [ 'num', 'name' ] }), queryMode : 'local', displayField: 'name', valueField : 'num', allMonths : ['', '', '', '', '', '', '', '', '', '', '', '' ], months: [], initComponent: function() { /** *     ,      {@link MonthComboBox#months} */ for (var i = 0; i < this.months.length; i++) { this.store.add({ num : this.months[i], name: this.allMonths[this.months[i]] }); } this.callParent(arguments); } }); 


Create an HTML page ExtjsTesterPage.html, within which the component will be tested. To include ExtJS stylesheets and scripts in a page, you must at least import the ext-all.css and ext-all.js files. For this purpose, we use the version of ExtJS 4.0.2a, which is hosted on CacheFly.

 <!DOCTYPE html> <html> <head> <title>ExtJS Test Page</title> <link rel="stylesheet" type="text/css" href="http://extjs.cachefly.net/ext-4.0.2a/resources/css/ext-all.css"> <script type="text/javascript" src="http://extjs.cachefly.net/ext-4.0.2a/ext-all.js"></script> </head> <body></body> </html> 


Let's prepare a small sample framework for testing. This is the ExtjsTester.js file that will be sent to PhantomJS for execution and take the name of the script with the test as an argument. At the beginning of it we will place a check for the presence of a command line argument:

 if (phantom.args.length == 0) { console.log('      '); phantom.exit(1); } 

Using this code snippet, two important elements of the PhantomJS API can be illustrated. First, passing parameters to the script: phantom.args is an array of command line arguments. The second API element is a call to the phantom.exit () method, which causes PhantomJS to terminate and return the specified error code.

We will connect the module of work with the file system, which we will need to load scripts in runtime mode. The fs module has a fairly extensive functionality that allows you to search, read and write files.
 var fs = require('fs'); 


For the sake of simplicity, I did not include any existing libraries for functional JavaScript testing, although PhantomJS has integration with them. Let's declare an assert exception type class:
 function AssertionError(message) { this.message = message; } AssertionError.prototype.toString = function() { return 'AssertionError: ' + this.message; }; 


Now for testing purposes we will create a wrapper over the standard PhantomJS class - WebPage.

 function TestPage(scriptUnderTest, testFun) { //      if (!fs.exists(scriptUnderTest)) { console.log("File " + scriptUnderTest + " not found"); phantom.exit(1); } var me = this; //   this.page = require('webpage').create(); //           this.page.onConsoleMessage = function (msg) { console.log(msg); }; //    this.page.open("ExtjsTesterPage.html", function() { //    me.page.injectJs(scriptUnderTest); //   ExtJS     me.waitForExtReady(function() { me.doTest(testFun); }); }); } 


The parameter of the scriptUnderTest constructor is the name of the file with the script to be tested, for example, with the ExtJS component code. Using the previously imported fs dependency, we make sure that this file exists, and using the WebPage # injectJs function, we inject it into our test page, which we downloaded from the previously created ExtjsTesterPage.html file.

It is worth paying attention to the WebPage # onConsoleMessage method. If you do not specify it, the entire console output that occurred on the page will be lost, since the page is running in the sandbox.

The testFun parameter is our future test function. In order to ensure correct testing of the ExtJS component, you need to make sure that the ExtJS framework is fully loaded and initialized. The Ext.onReady () method is intended for this, but in our case it is difficult to use it because the page runs in the sandbox, and passing any code into the page is only possible using the WebPage # evaluate method. Therefore, let's equip the TestPage class with a method waiting for ExtJS to load:

 TestPage.prototype.waitForExtReady = function(fun) { var me = this; console.log(' ExtJS...'); var readyChecker = window.setInterval(function() { var isReady = me.page.evaluate(function() { return Ext.isReady; }); if (isReady) { console.log('ExtJS .'); window.clearInterval(readyChecker); fun.call(me); } }, 100); }; 


When ExtJS is fully loaded, the doTest method will be executed as follows:

 TestPage.prototype.doTest = function(testFun) { try { //    testFun.call(this); phantom.exit(0); } catch (e) { console.log(e); phantom.exit(1); } }; 


Here, the test function call and the completion of PhantomJS with a return code corresponding to the test function results are implemented.

Finally, we will add to the TestPage class several conservative methods for assertions and code execution in the context of the page:

 TestPage.prototype.evaluate = function(fun) { return this.page.evaluate(fun); }; TestPage.prototype.assert = function assert(test, message) { if (!test) { throw new AssertionError(message); } }; TestPage.prototype.evaluateAndAssertEquals = function(expectedValue, actualFun, message) { this.assert(expectedValue === this.evaluate(actualFun), message); }; 


The evaluate method delegates the execution of the passed function to the wrapped WebPage object. The assert method executes a typical diagnostic statement with an AssertionError overrun if this assertion fails. The evaluateAndAssertEquals method compares the result of executing a function in the context of the page with the expected value.

The last line of the ExtjsTester file will be the download of the test script itself, which we will pass through the command line parameter. We will use the module for working with the file system for this - we will load the script as a string and execute it via eval:

 eval(fs.read(phantom.args[0])); 


And here is the test script itself - the file MonthComboBoxTest.js:
 new TestPage("MonthComboBox.js", function() { //   MonthComboBox      this.evaluate(function() { Ext.widget('monthcombo', { months : [1, 2, 5], renderTo: Ext.getBody() }); }); // ,        this.evaluateAndAssertEquals(3, function() { return Ext.ComponentQuery.query('monthcombo')[0].store.getCount(); }, "Wrong element count"); }); 


Here we create our TestPage wrapper object, passing it the script of the component being tested and a function for testing. In it, we first create a component on a page with such a configuration that it contains only three months, and then check that our design is true, the component is really present on the page, and its storage contains exactly three elements.

To run the test, use the command:

phantomjs ExtjsTester.js MonthComboBoxTest.js


Application Testing



First of all, to test the application, we need to modify our framework a bit. The test function should be executed upon completion of the application launch. One way to determine this is to check the presence of the Viewport component on the page. Add the following method to the TestPage class, which is basically the same as the TestPage # waitForExtReady method:

 TestPage.prototype.waitForViewport = function(fun) { var me = this; console.log('  Viewport...'); var launchedChecker = window.setInterval(function() { var isLaunched = me.page.evaluate(function() { return typeof Ext.ComponentQuery.query('viewport')[0] !== 'undefined'; }); if (isLaunched) { console.log('Viewport .'); window.clearInterval(launchedChecker); fun.call(me); } }, 100); }; 


We also add the waitForViewport parameter to the constructor of the TestPage object, the value of which true will mean that it is necessary to wait for the appearance of the Viewport before executing the test function. We slightly modify the fragment in which the test function is called:
 me.waitForExtReady(function() { if (waitForViewport) { me.waitForViewport(function() { me.doTest(testFun); }) } else { me.doTest(testFun); } }); 


For the sake of brevity, I will not give here all the code of the application under test - you can take a look at it on GitHub . I will explain only that the application consists of a main form with a button and a window that appears when you click on this button. The main form and the window are served by various controllers, the connection between them is organized through application-level messages (Ext.app.Application # fireEvent). Here is a test code that checks that when you click a button, a pop-up window really appears:

 new TestPage("app.js", function() { //     this.evaluate(function() { Ext.ComponentQuery.query('mainform > button[action=popup]')[0].btnEl.dom.click(); }); // ,      this.evaluateAndAssertTrue(function() { return typeof Ext.ComponentQuery.query('popupwindow')[0] !== 'undefined'; }, "Popup window not opened"); }, true); 


The component query 'mainform> button [action = popup]' matches the button on the main form of the application. The request 'popupwindow' corresponds to a popup window. The third parameter of the TestPage constructor (true value) means that you need to wait for the appearance of the Viewport before executing the test function. The test function starts similarly:
phantomjs ExtjsTester.js AppTest.js


The article illustrates an approach to testing components and applications on ExtJS / Sencha based on the PhantomJS engine and has developed an example preparation for a testing framework, which can equally well be used for testing other JavaScript libraries. In conclusion, I would like to note that such functional testing is limited to browsers running on the WebKit engine. However, given the high standards of compliance of this engine with modern Web standards, as well as an acceptable level of abstraction and cross-browser compatibility of the ExtJS library (Sencha), it can be concluded that the PhantomJS component may be sufficient for testing the logic of the application’s operation or functioning.

The code of the test component and the application, as well as the testing framework presented in the article, is available on GitHub .

Source: https://habr.com/ru/post/141165/


All Articles