Merge "Localisation updates from https://translatewiki.net."
[mediawiki.git] / tests / qunit / data / testrunner.js
blob1a2bfa102d625ac237aebaf81402f4f665c93472
1 /*global CompletenessTest */
2 /*jshint evil: true */
3 ( function ( $, mw, QUnit, undefined ) {
4         'use strict';
6         var mwTestIgnore, mwTester,
7                 addons,
8                 envExecCount,
9                 ELEMENT_NODE = 1,
10                 TEXT_NODE = 3;
12         /**
13          * Add bogus to url to prevent IE crazy caching
14          *
15          * @param value {String} a relative path (eg. 'data/foo.js'
16          * or 'data/test.php?foo=bar').
17          * @return {String} Such as 'data/foo.js?131031765087663960'
18          */
19         QUnit.fixurl = function ( value ) {
20                 return value + (/\?/.test( value ) ? '&' : '?')
21                         + String( new Date().getTime() )
22                         + String( parseInt( Math.random() * 100000, 10 ) );
23         };
25         /**
26          * Configuration
27          */
29         // When a test() indicates asynchronicity with stop(),
30         // allow 10 seconds to pass before killing the test(),
31         // and assuming failure.
32         QUnit.config.testTimeout = 10 * 1000;
34         // Add a checkbox to QUnit header to toggle MediaWiki ResourceLoader debug mode.
35         QUnit.config.urlConfig.push( {
36                 id: 'debug',
37                 label: 'Enable ResourceLoaderDebug',
38                 tooltip: 'Enable debug mode in ResourceLoader'
39         } );
41         QUnit.config.requireExpects = true;
43         /**
44          * Load TestSwarm agent
45          */
46         // Only if the current url indicates that there is a TestSwarm instance watching us
47         // (TestSwarm appends swarmURL to the test suites url it loads in iframes).
48         // Otherwise this is just a simple view of Special:JavaScriptTest/qunit directly,
49         // no point in loading inject.js in that case. Also, make sure that this instance
50         // of MediaWiki has actually been configured with the required url to that inject.js
51         // script. By default it is false.
52         if ( QUnit.urlParams.swarmURL && mw.config.get( 'QUnitTestSwarmInjectJSPath' ) ) {
53                 jQuery.getScript( QUnit.fixurl( mw.config.get( 'QUnitTestSwarmInjectJSPath' ) ) );
54         }
56         /**
57          * CompletenessTest
58          *
59          * Adds toggle checkbox to header
60          */
61         QUnit.config.urlConfig.push( {
62                 id: 'completenesstest',
63                 label: 'Run CompletenessTest',
64                 tooltip: 'Run the completeness test'
65         } );
67         // Initiate when enabled
68         if ( QUnit.urlParams.completenesstest ) {
70                 // Return true to ignore
71                 mwTestIgnore = function ( val, tester ) {
73                         // Don't record methods of the properties of constructors,
74                         // to avoid getting into a loop (prototype.constructor.prototype..).
75                         // Since we're therefor skipping any injection for
76                         // "new mw.Foo()", manually set it to true here.
77                         if ( val instanceof mw.Map ) {
78                                 tester.methodCallTracker.Map = true;
79                                 return true;
80                         }
81                         if ( val instanceof mw.Title ) {
82                                 tester.methodCallTracker.Title = true;
83                                 return true;
84                         }
86                         // Don't record methods of the properties of a jQuery object
87                         if ( val instanceof $ ) {
88                                 return true;
89                         }
91                         return false;
92                 };
94                 mwTester = new CompletenessTest( mw, mwTestIgnore );
95         }
97         /**
98          * Test environment recommended for all QUnit test modules
99          *
100          * Whether to log environment changes to the console
101          */
102         QUnit.config.urlConfig.push( 'mwlogenv' );
104         /**
105          * Reset mw.config and others to a fresh copy of the live config for each test(),
106          * and restore it back to the live one afterwards.
107          * @param localEnv {Object} [optional]
108          * @example (see test suite at the bottom of this file)
109          * </code>
110          */
111         QUnit.newMwEnvironment = ( function () {
112                 var log, liveConfig, liveMessages;
114                 liveConfig = mw.config.values;
115                 liveMessages = mw.messages.values;
117                 function freshConfigCopy( custom ) {
118                         // Tests should mock all factors that directly influence the tested code.
119                         // For backwards compatibility though we set mw.config to a copy of the live config
120                         // and extend it with the (optionally) given custom settings for this test
121                         // (instead of starting blank with only the given custmo settings).
122                         // This is a shallow copy, so we don't end up with settings taking an array value
123                         // extended with the custom settings - setting a config property means you override it,
124                         // not extend it.
125                         return $.extend( {}, liveConfig, custom );
126                 }
128                 function freshMessagesCopy( custom ) {
129                         return $.extend( /*deep=*/true, {}, liveMessages, custom );
130                 }
132                 log = QUnit.urlParams.mwlogenv ? mw.log : function () {};
134                 return function ( localEnv ) {
135                         localEnv = $.extend( {
136                                 // QUnit
137                                 setup: $.noop,
138                                 teardown: $.noop,
139                                 // MediaWiki
140                                 config: {},
141                                 messages: {}
142                         }, localEnv );
144                         return {
145                                 setup: function () {
146                                         log( 'MwEnvironment> SETUP    for "' + QUnit.config.current.module
147                                                 + ': ' + QUnit.config.current.testName + '"' );
149                                         // Greetings, mock environment!
150                                         mw.config.values = freshConfigCopy( localEnv.config );
151                                         mw.messages.values = freshMessagesCopy( localEnv.messages );
153                                         localEnv.setup();
154                                 },
156                                 teardown: function () {
157                                         log( 'MwEnvironment> TEARDOWN for "' + QUnit.config.current.module
158                                                 + ': ' + QUnit.config.current.testName + '"' );
160                                         localEnv.teardown();
162                                         // Farewell, mock environment!
163                                         mw.config.values = liveConfig;
164                                         mw.messages.values = liveMessages;
165                                 }
166                         };
167                 };
168         }() );
170         // $.when stops as soon as one fails, which makes sense in most
171         // practical scenarios, but not in a unit test where we really do
172         // need to wait until all of them are finished.
173         QUnit.whenPromisesComplete = function () {
174                 var altPromises = [];
176                 $.each( arguments, function ( i, arg ) {
177                         var alt = $.Deferred();
178                         altPromises.push( alt );
180                         // Whether this one fails or not, forwards it to
181                         // the 'done' (resolve) callback of the alternative promise.
182                         arg.always( alt.resolve );
183                 } );
185                 return $.when.apply( $, altPromises );
186         };
188         /**
189          * Recursively convert a node to a plain object representing its structure.
190          * Only considers attributes and contents (elements and text nodes).
191          * Attribute values are compared strictly and not normalised.
192          *
193          * @param {Node} node
194          * @return {Object|string} Plain JavaScript value representing the node.
195          */
196         function getDomStructure( node ) {
197                 var $node, children, processedChildren, i, len, el;
198                 $node = $( node );
199                 if ( node.nodeType === ELEMENT_NODE ) {
200                         children = $node.contents();
201                         processedChildren = [];
202                         for ( i = 0, len = children.length; i < len; i++ ) {
203                                 el = children[i];
204                                 if ( el.nodeType === ELEMENT_NODE || el.nodeType === TEXT_NODE ) {
205                                         processedChildren.push( getDomStructure( el ) );
206                                 }
207                         }
209                         return {
210                                 tagName: node.tagName,
211                                 attributes: $node.getAttrs(),
212                                 contents: processedChildren
213                         };
214                 } else {
215                         // Should be text node
216                         return $node.text();
217                 }
218         }
220         /**
221          * Gets structure of node for this HTML.
222          *
223          * @param {string} html HTML markup for one or more nodes.
224          */
225         function getHtmlStructure( html ) {
226                 var el = $( '<div>' ).append( html )[0];
227                 return getDomStructure( el );
228         }
230         /**
231          * Add-on assertion helpers
232          */
233         // Define the add-ons
234         addons = {
236                 // Expect boolean true
237                 assertTrue: function ( actual, message ) {
238                         QUnit.push( actual === true, actual, true, message );
239                 },
241                 // Expect boolean false
242                 assertFalse: function ( actual, message ) {
243                         QUnit.push( actual === false, actual, false, message );
244                 },
246                 // Expect numerical value less than X
247                 lt: function ( actual, expected, message ) {
248                         QUnit.push( actual < expected, actual, 'less than ' + expected, message );
249                 },
251                 // Expect numerical value less than or equal to X
252                 ltOrEq: function ( actual, expected, message ) {
253                         QUnit.push( actual <= expected, actual, 'less than or equal to ' + expected, message );
254                 },
256                 // Expect numerical value greater than X
257                 gt: function ( actual, expected, message ) {
258                         QUnit.push( actual > expected, actual, 'greater than ' + expected, message );
259                 },
261                 // Expect numerical value greater than or equal to X
262                 gtOrEq: function ( actual, expected, message ) {
263                         QUnit.push( actual >= expected, actual, 'greater than or equal to ' + expected, message );
264                 },
266                 /**
267                  * Asserts that two HTML strings are structurally equivalent.
268                  *
269                  * @param {string} actualHtml Actual HTML markup.
270                  * @param {string} expectedHtml Expected HTML markup
271                  * @param {string} message Assertion message.
272                  */
273                 htmlEqual: function ( actualHtml, expectedHtml, message ) {
274                         var actual = getHtmlStructure( actualHtml ),
275                                 expected = getHtmlStructure( expectedHtml );
277                         QUnit.push(
278                                 QUnit.equiv(
279                                         actual,
280                                         expected
281                                 ),
282                                 actual,
283                                 expected,
284                                 message
285                         );
286                 },
288                 /**
289                  * Asserts that two HTML strings are not structurally equivalent.
290                  *
291                  * @param {string} actualHtml Actual HTML markup.
292                  * @param {string} expectedHtml Expected HTML markup.
293                  * @param {string} message Assertion message.
294                  */
295                 notHtmlEqual: function ( actualHtml, expectedHtml, message ) {
296                         var actual = getHtmlStructure( actualHtml ),
297                                 expected = getHtmlStructure( expectedHtml );
299                         QUnit.push(
300                                 !QUnit.equiv(
301                                         actual,
302                                         expected
303                                 ),
304                                 actual,
305                                 expected,
306                                 message
307                         );
308                 }
309         };
311         $.extend( QUnit.assert, addons );
313         /**
314          * Small test suite to confirm proper functionality of the utilities and
315          * initializations defined above in this file.
316          */
317         envExecCount = 0;
318         QUnit.module( 'mediawiki.tests.qunit.testrunner', QUnit.newMwEnvironment( {
319                 setup: function () {
320                         envExecCount += 1;
321                         this.mwHtmlLive = mw.html;
322                         mw.html = {
323                                 escape: function () {
324                                         return 'mocked-' + envExecCount;
325                                 }
326                         };
327                 },
328                 teardown: function () {
329                         mw.html = this.mwHtmlLive;
330                 },
331                 config: {
332                         testVar: 'foo'
333                 },
334                 messages: {
335                         testMsg: 'Foo.'
336                 }
337         } ) );
339         QUnit.test( 'Setup', 3, function ( assert ) {
340                 assert.equal( mw.html.escape( 'foo' ), 'mocked-1', 'extra setup() callback was ran.' );
341                 assert.equal( mw.config.get( 'testVar' ), 'foo', 'config object applied' );
342                 assert.equal( mw.messages.get( 'testMsg' ), 'Foo.', 'messages object applied' );
344                 mw.config.set( 'testVar', 'bar' );
345                 mw.messages.set( 'testMsg', 'Bar.' );
346         } );
348         QUnit.test( 'Teardown', 3, function ( assert ) {
349                 assert.equal( mw.html.escape( 'foo' ), 'mocked-2', 'extra setup() callback was re-ran.' );
350                 assert.equal( mw.config.get( 'testVar' ), 'foo', 'config object restored and re-applied after test()' );
351                 assert.equal( mw.messages.get( 'testMsg' ), 'Foo.', 'messages object restored and re-applied after test()' );
352         } );
354         QUnit.test( 'Loader status', 2, function ( assert ) {
355                 var i, len, state,
356                         modules = mw.loader.getModuleNames(),
357                         error = [],
358                         missing = [];
360                 for ( i = 0, len = modules.length; i < len; i++ ) {
361                         state = mw.loader.getState( modules[i] );
362                         if ( state === 'error' ) {
363                                 error.push( modules[i] );
364                         } else if ( state === 'missing' ) {
365                                 missing.push( modules[i] );
366                         }
367                 }
369                 assert.deepEqual( error, [], 'Modules in error state' );
370                 assert.deepEqual( missing, [], 'Modules in missing state' );
371         } );
373         QUnit.test( 'htmlEqual', 8, function ( assert ) {
374                 assert.htmlEqual(
375                         '<div><p class="some classes" data-length="10">Child paragraph with <a href="http://example.com">A link</a></p>Regular text<span>A span</span></div>',
376                         '<div><p data-length=\'10\'  class=\'some classes\'>Child paragraph with <a href=\'http://example.com\' >A link</a></p>Regular text<span>A span</span></div>',
377                         'Attribute order, spacing and quotation marks (equal)'
378                 );
380                 assert.notHtmlEqual(
381                         '<div><p class="some classes" data-length="10">Child paragraph with <a href="http://example.com">A link</a></p>Regular text<span>A span</span></div>',
382                         '<div><p data-length=\'10\'  class=\'some more classes\'>Child paragraph with <a href=\'http://example.com\' >A link</a></p>Regular text<span>A span</span></div>',
383                         'Attribute order, spacing and quotation marks (not equal)'
384                 );
386                 assert.htmlEqual(
387                         '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
388                         '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
389                         'Multiple root nodes (equal)'
390                 );
392                 assert.notHtmlEqual(
393                         '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
394                         '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="important" >Last</label><input id="lastname" />',
395                         'Multiple root nodes (not equal, last label node is different)'
396                 );
398                 assert.htmlEqual(
399                         'fo&quot;o<br/>b&gt;ar',
400                         'fo"o<br/>b>ar',
401                         'Extra escaping is equal'
402                 );
403                 assert.notHtmlEqual(
404                         'foo&lt;br/&gt;bar',
405                         'foo<br/>bar',
406                         'Text escaping (not equal)'
407                 );
409                 assert.htmlEqual(
410                         'foo<a href="http://example.com">example</a>bar',
411                         'foo<a href="http://example.com">example</a>bar',
412                         'Outer text nodes are compared (equal)'
413                 );
415                 assert.notHtmlEqual(
416                         'foo<a href="http://example.com">example</a>bar',
417                         'foo<a href="http://example.com">example</a>quux',
418                         'Outer text nodes are compared (last text node different)'
419                 );
421         } );
423         QUnit.module( 'mediawiki.tests.qunit.testrunner-after', QUnit.newMwEnvironment() );
425         QUnit.test( 'Teardown', 3, function ( assert ) {
426                 assert.equal( mw.html.escape( '<' ), '&lt;', 'extra teardown() callback was ran.' );
427                 assert.equal( mw.config.get( 'testVar' ), null, 'config object restored to live in next module()' );
428                 assert.equal( mw.messages.get( 'testMsg' ), null, 'messages object restored to live in next module()' );
429         } );
431 }( jQuery, mediaWiki, QUnit ) );