Merge "DatabaseMssql: Don't duplicate body of makeList()"
[mediawiki.git] / tests / qunit / data / testrunner.js
blob03aaf4af700a7503b6f7826a7893ad9b7596297b
1 /*global CompletenessTest, sinon */
2 /*jshint evil: true */
3 ( function ( $, mw, QUnit ) {
4         'use strict';
6         var mwTestIgnore, mwTester,
7                 addons,
8                 ELEMENT_NODE = 1,
9                 TEXT_NODE = 3;
11         /**
12          * Add bogus to url to prevent IE crazy caching
13          *
14          * @param value {String} a relative path (eg. 'data/foo.js'
15          * or 'data/test.php?foo=bar').
16          * @return {String} Such as 'data/foo.js?131031765087663960'
17          */
18         QUnit.fixurl = function ( value ) {
19                 return value + (/\?/.test( value ) ? '&' : '?')
20                         + String( new Date().getTime() )
21                         + String( parseInt( Math.random() * 100000, 10 ) );
22         };
24         /**
25          * Configuration
26          */
28         // When a test() indicates asynchronicity with stop(),
29         // allow 30 seconds to pass before killing the test(),
30         // and assuming failure.
31         QUnit.config.testTimeout = 30 * 1000;
33         QUnit.config.requireExpects = true;
35         // Add a checkbox to QUnit header to toggle MediaWiki ResourceLoader debug mode.
36         QUnit.config.urlConfig.push( {
37                 id: 'debug',
38                 label: 'Enable ResourceLoaderDebug',
39                 tooltip: 'Enable debug mode in ResourceLoader',
40                 value: 'true'
41         } );
43         /**
44          * CompletenessTest
45          *
46          * Adds toggle checkbox to header
47          */
48         QUnit.config.urlConfig.push( {
49                 id: 'completenesstest',
50                 label: 'Run CompletenessTest',
51                 tooltip: 'Run the completeness test'
52         } );
54         /**
55          * SinonJS
56          *
57          * Glue code for nicer integration with QUnit setup/teardown
58          * Inspired by http://sinonjs.org/releases/sinon-qunit-1.0.0.js
59          * Fixes:
60          * - Work properly with asynchronous QUnit by using module setup/teardown
61          *   instead of synchronously wrapping QUnit.test.
62          */
63         sinon.assert.fail = function ( msg ) {
64                 QUnit.assert.ok( false, msg );
65         };
66         sinon.assert.pass = function ( msg ) {
67                 QUnit.assert.ok( true, msg );
68         };
69         sinon.config = {
70                 injectIntoThis: true,
71                 injectInto: null,
72                 properties: ['spy', 'stub', 'mock', 'sandbox'],
73                 // Don't fake timers by default
74                 useFakeTimers: false,
75                 useFakeServer: false
76         };
77         ( function () {
78                 var orgModule = QUnit.module;
80                 QUnit.module = function ( name, localEnv ) {
81                         localEnv = localEnv || {};
82                         orgModule( name, {
83                                 setup: function () {
84                                         var config = sinon.getConfig( sinon.config );
85                                         config.injectInto = this;
86                                         sinon.sandbox.create( config );
88                                         if ( localEnv.setup ) {
89                                                 localEnv.setup.call( this );
90                                         }
91                                 },
92                                 teardown: function () {
93                                         this.sandbox.verifyAndRestore();
95                                         if ( localEnv.teardown ) {
96                                                 localEnv.teardown.call( this );
97                                         }
98                                 }
99                         } );
100                 };
101         }() );
103         // Extend QUnit.module to provide a fixture element.
104         ( function () {
105                 var orgModule = QUnit.module;
107                 QUnit.module = function ( name, localEnv ) {
108                         var fixture;
109                         localEnv = localEnv || {};
110                         orgModule( name, {
111                                 setup: function () {
112                                         fixture = document.createElement( 'div' );
113                                         fixture.id = 'qunit-fixture';
114                                         document.body.appendChild( fixture );
116                                         if ( localEnv.setup ) {
117                                                 localEnv.setup.call( this );
118                                         }
119                                 },
120                                 teardown: function () {
121                                         if ( localEnv.teardown ) {
122                                                 localEnv.teardown.call( this );
123                                         }
125                                         fixture.parentNode.removeChild( fixture );
126                                 }
127                         } );
128                 };
129         }() );
131         // Initiate when enabled
132         if ( QUnit.urlParams.completenesstest ) {
134                 // Return true to ignore
135                 mwTestIgnore = function ( val, tester ) {
137                         // Don't record methods of the properties of constructors,
138                         // to avoid getting into a loop (prototype.constructor.prototype..).
139                         // Since we're therefor skipping any injection for
140                         // "new mw.Foo()", manually set it to true here.
141                         if ( val instanceof mw.Map ) {
142                                 tester.methodCallTracker.Map = true;
143                                 return true;
144                         }
145                         if ( val instanceof mw.Title ) {
146                                 tester.methodCallTracker.Title = true;
147                                 return true;
148                         }
150                         // Don't record methods of the properties of a jQuery object
151                         if ( val instanceof $ ) {
152                                 return true;
153                         }
155                         // Don't iterate over the module registry (the 'script' references would
156                         // be listed as untested methods otherwise)
157                         if ( val === mw.loader.moduleRegistry ) {
158                                 return true;
159                         }
161                         return false;
162                 };
164                 mwTester = new CompletenessTest( mw, mwTestIgnore );
165         }
167         /**
168          * Test environment recommended for all QUnit test modules
169          *
170          * Whether to log environment changes to the console
171          */
172         QUnit.config.urlConfig.push( 'mwlogenv' );
174         /**
175          * Reset mw.config and others to a fresh copy of the live config for each test(),
176          * and restore it back to the live one afterwards.
177          * @param localEnv {Object} [optional]
178          * @example (see test suite at the bottom of this file)
179          * </code>
180          */
181         QUnit.newMwEnvironment = ( function () {
182                 var warn, log, liveConfig, liveMessages;
184                 liveConfig = mw.config.values;
185                 liveMessages = mw.messages.values;
187                 function suppressWarnings() {
188                         warn = mw.log.warn;
189                         mw.log.warn = $.noop;
190                 }
192                 function restoreWarnings() {
193                         if ( warn !== undefined ) {
194                                 mw.log.warn = warn;
195                                 warn = undefined;
196                         }
197                 }
199                 function freshConfigCopy( custom ) {
200                         var copy;
201                         // Tests should mock all factors that directly influence the tested code.
202                         // For backwards compatibility though we set mw.config to a fresh copy of the live
203                         // config. This way any modifications made to mw.config during the test will not
204                         // affect other tests, nor the global scope outside the test runner.
205                         // This is a shallow copy, since overriding an array or object value via "custom"
206                         // should replace it. Setting a config property means you override it, not extend it.
207                         // NOTE: It is important that we suppress warnings because extend() will also access
208                         // deprecated properties and trigger deprecation warnings from mw.log#deprecate.
209                         suppressWarnings();
210                         copy = $.extend( {}, liveConfig, custom );
211                         restoreWarnings();
213                         return copy;
214                 }
216                 function freshMessagesCopy( custom ) {
217                         return $.extend( /*deep=*/true, {}, liveMessages, custom );
218                 }
220                 log = QUnit.urlParams.mwlogenv ? mw.log : function () {};
222                 return function ( localEnv ) {
223                         localEnv = $.extend( {
224                                 // QUnit
225                                 setup: $.noop,
226                                 teardown: $.noop,
227                                 // MediaWiki
228                                 config: {},
229                                 messages: {}
230                         }, localEnv );
232                         return {
233                                 setup: function () {
234                                         log( 'MwEnvironment> SETUP    for "' + QUnit.config.current.module
235                                                 + ': ' + QUnit.config.current.testName + '"' );
237                                         // Greetings, mock environment!
238                                         mw.config.values = freshConfigCopy( localEnv.config );
239                                         mw.messages.values = freshMessagesCopy( localEnv.messages );
240                                         this.suppressWarnings = suppressWarnings;
241                                         this.restoreWarnings = restoreWarnings;
243                                         localEnv.setup.call( this );
244                                 },
246                                 teardown: function () {
247                                         var timers;
248                                         log( 'MwEnvironment> TEARDOWN for "' + QUnit.config.current.module
249                                                 + ': ' + QUnit.config.current.testName + '"' );
251                                         localEnv.teardown.call( this );
253                                         // Farewell, mock environment!
254                                         mw.config.values = liveConfig;
255                                         mw.messages.values = liveMessages;
257                                         // As a convenience feature, automatically restore warnings if they're
258                                         // still suppressed by the end of the test.
259                                         restoreWarnings();
261                                         // Check for incomplete animations/requests/etc and throw
262                                         // error if there are any.
263                                         if ( $.timers && $.timers.length !== 0 ) {
264                                                 timers = $.timers.length;
265                                                 // Tests shoulld use fake timers or wait for animations to complete
266                                                 $.each( $.timers, function ( i, timer ) {
267                                                         var node = timer.elem;
268                                                         mw.log.warn( 'Unfinished animation #' + i + ' in ' + timer.queue + ' queue on ' +
269                                                                 mw.html.element( node.nodeName.toLowerCase(), $(node).getAttrs() )
270                                                         );
271                                                 } );
272                                                 // Force animations to stop to give the next test a clean start
273                                                 $.fx.stop();
275                                                 throw new Error( 'Unfinished animations: ' + timers );
276                                         }
277                                         if ( $.active !== undefined && $.active !== 0 ) {
278                                                 // Test may need to use fake XHR, wait for requests or
279                                                 // call abort().
280                                                 throw new Error( 'Unfinished AJAX requests: ' + $.active );
281                                         }
282                                 }
283                         };
284                 };
285         }() );
287         // $.when stops as soon as one fails, which makes sense in most
288         // practical scenarios, but not in a unit test where we really do
289         // need to wait until all of them are finished.
290         QUnit.whenPromisesComplete = function () {
291                 var altPromises = [];
293                 $.each( arguments, function ( i, arg ) {
294                         var alt = $.Deferred();
295                         altPromises.push( alt );
297                         // Whether this one fails or not, forwards it to
298                         // the 'done' (resolve) callback of the alternative promise.
299                         arg.always( alt.resolve );
300                 } );
302                 return $.when.apply( $, altPromises );
303         };
305         /**
306          * Recursively convert a node to a plain object representing its structure.
307          * Only considers attributes and contents (elements and text nodes).
308          * Attribute values are compared strictly and not normalised.
309          *
310          * @param {Node} node
311          * @return {Object|string} Plain JavaScript value representing the node.
312          */
313         function getDomStructure( node ) {
314                 var $node, children, processedChildren, i, len, el;
315                 $node = $( node );
316                 if ( node.nodeType === ELEMENT_NODE ) {
317                         children = $node.contents();
318                         processedChildren = [];
319                         for ( i = 0, len = children.length; i < len; i++ ) {
320                                 el = children[i];
321                                 if ( el.nodeType === ELEMENT_NODE || el.nodeType === TEXT_NODE ) {
322                                         processedChildren.push( getDomStructure( el ) );
323                                 }
324                         }
326                         return {
327                                 tagName: node.tagName,
328                                 attributes: $node.getAttrs(),
329                                 contents: processedChildren
330                         };
331                 } else {
332                         // Should be text node
333                         return $node.text();
334                 }
335         }
337         /**
338          * Gets structure of node for this HTML.
339          *
340          * @param {string} html HTML markup for one or more nodes.
341          */
342         function getHtmlStructure( html ) {
343                 var el = $( '<div>' ).append( html )[0];
344                 return getDomStructure( el );
345         }
347         /**
348          * Add-on assertion helpers
349          */
350         // Define the add-ons
351         addons = {
353                 // Expect boolean true
354                 assertTrue: function ( actual, message ) {
355                         QUnit.push( actual === true, actual, true, message );
356                 },
358                 // Expect boolean false
359                 assertFalse: function ( actual, message ) {
360                         QUnit.push( actual === false, actual, false, message );
361                 },
363                 // Expect numerical value less than X
364                 lt: function ( actual, expected, message ) {
365                         QUnit.push( actual < expected, actual, 'less than ' + expected, message );
366                 },
368                 // Expect numerical value less than or equal to X
369                 ltOrEq: function ( actual, expected, message ) {
370                         QUnit.push( actual <= expected, actual, 'less than or equal to ' + expected, message );
371                 },
373                 // Expect numerical value greater than X
374                 gt: function ( actual, expected, message ) {
375                         QUnit.push( actual > expected, actual, 'greater than ' + expected, message );
376                 },
378                 // Expect numerical value greater than or equal to X
379                 gtOrEq: function ( actual, expected, message ) {
380                         QUnit.push( actual >= expected, actual, 'greater than or equal to ' + expected, message );
381                 },
383                 /**
384                  * Asserts that two HTML strings are structurally equivalent.
385                  *
386                  * @param {string} actualHtml Actual HTML markup.
387                  * @param {string} expectedHtml Expected HTML markup
388                  * @param {string} message Assertion message.
389                  */
390                 htmlEqual: function ( actualHtml, expectedHtml, message ) {
391                         var actual = getHtmlStructure( actualHtml ),
392                                 expected = getHtmlStructure( expectedHtml );
394                         QUnit.push(
395                                 QUnit.equiv(
396                                         actual,
397                                         expected
398                                 ),
399                                 actual,
400                                 expected,
401                                 message
402                         );
403                 },
405                 /**
406                  * Asserts that two HTML strings are not structurally equivalent.
407                  *
408                  * @param {string} actualHtml Actual HTML markup.
409                  * @param {string} expectedHtml Expected HTML markup.
410                  * @param {string} message Assertion message.
411                  */
412                 notHtmlEqual: function ( actualHtml, expectedHtml, message ) {
413                         var actual = getHtmlStructure( actualHtml ),
414                                 expected = getHtmlStructure( expectedHtml );
416                         QUnit.push(
417                                 !QUnit.equiv(
418                                         actual,
419                                         expected
420                                 ),
421                                 actual,
422                                 expected,
423                                 message
424                         );
425                 }
426         };
428         $.extend( QUnit.assert, addons );
430         /**
431          * Small test suite to confirm proper functionality of the utilities and
432          * initializations defined above in this file.
433          */
434         QUnit.module( 'test.mediawiki.qunit.testrunner', QUnit.newMwEnvironment( {
435                 setup: function () {
436                         this.mwHtmlLive = mw.html;
437                         mw.html = {
438                                 escape: function () {
439                                         return 'mocked';
440                                 }
441                         };
442                 },
443                 teardown: function () {
444                         mw.html = this.mwHtmlLive;
445                 },
446                 config: {
447                         testVar: 'foo'
448                 },
449                 messages: {
450                         testMsg: 'Foo.'
451                 }
452         } ) );
454         QUnit.test( 'Setup', 3, function ( assert ) {
455                 assert.equal( mw.html.escape( 'foo' ), 'mocked', 'setup() callback was ran.' );
456                 assert.equal( mw.config.get( 'testVar' ), 'foo', 'config object applied' );
457                 assert.equal( mw.messages.get( 'testMsg' ), 'Foo.', 'messages object applied' );
459                 mw.config.set( 'testVar', 'bar' );
460                 mw.messages.set( 'testMsg', 'Bar.' );
461         } );
463         QUnit.test( 'Teardown', 2, function ( assert ) {
464                 assert.equal( mw.config.get( 'testVar' ), 'foo', 'config object restored and re-applied after test()' );
465                 assert.equal( mw.messages.get( 'testMsg' ), 'Foo.', 'messages object restored and re-applied after test()' );
466         } );
468         QUnit.test( 'Loader status', 2, function ( assert ) {
469                 var i, len, state,
470                         modules = mw.loader.getModuleNames(),
471                         error = [],
472                         missing = [];
474                 for ( i = 0, len = modules.length; i < len; i++ ) {
475                         state = mw.loader.getState( modules[i] );
476                         if ( state === 'error' ) {
477                                 error.push( modules[i] );
478                         } else if ( state === 'missing' ) {
479                                 missing.push( modules[i] );
480                         }
481                 }
483                 assert.deepEqual( error, [], 'Modules in error state' );
484                 assert.deepEqual( missing, [], 'Modules in missing state' );
485         } );
487         QUnit.test( 'htmlEqual', 8, function ( assert ) {
488                 assert.htmlEqual(
489                         '<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>',
490                         '<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>',
491                         'Attribute order, spacing and quotation marks (equal)'
492                 );
494                 assert.notHtmlEqual(
495                         '<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>',
496                         '<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>',
497                         'Attribute order, spacing and quotation marks (not equal)'
498                 );
500                 assert.htmlEqual(
501                         '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
502                         '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
503                         'Multiple root nodes (equal)'
504                 );
506                 assert.notHtmlEqual(
507                         '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
508                         '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="important" >Last</label><input id="lastname" />',
509                         'Multiple root nodes (not equal, last label node is different)'
510                 );
512                 assert.htmlEqual(
513                         'fo&quot;o<br/>b&gt;ar',
514                         'fo"o<br/>b>ar',
515                         'Extra escaping is equal'
516                 );
517                 assert.notHtmlEqual(
518                         'foo&lt;br/&gt;bar',
519                         'foo<br/>bar',
520                         'Text escaping (not equal)'
521                 );
523                 assert.htmlEqual(
524                         'foo<a href="http://example.com">example</a>bar',
525                         'foo<a href="http://example.com">example</a>bar',
526                         'Outer text nodes are compared (equal)'
527                 );
529                 assert.notHtmlEqual(
530                         'foo<a href="http://example.com">example</a>bar',
531                         'foo<a href="http://example.com">example</a>quux',
532                         'Outer text nodes are compared (last text node different)'
533                 );
535         } );
537         QUnit.module( 'test.mediawiki.qunit.testrunner-after', QUnit.newMwEnvironment() );
539         QUnit.test( 'Teardown', 3, function ( assert ) {
540                 assert.equal( mw.html.escape( '<' ), '&lt;', 'teardown() callback was ran.' );
541                 assert.equal( mw.config.get( 'testVar' ), null, 'config object restored to live in next module()' );
542                 assert.equal( mw.messages.get( 'testMsg' ), null, 'messages object restored to live in next module()' );
543         } );
545 }( jQuery, mediaWiki, QUnit ) );