Merge "Fix typo in Special:MIMESearch causing minor part to be ignored."
[mediawiki.git] / tests / qunit / data / testrunner.js
blobab9aab19d3df10693efd48a6be6b8146d46baec3
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 10 seconds to pass before killing the test(),
30         // and assuming failure.
31         QUnit.config.testTimeout = 10 * 1000;
33         // Add a checkbox to QUnit header to toggle MediaWiki ResourceLoader debug mode.
34         QUnit.config.urlConfig.push( {
35                 id: 'debug',
36                 label: 'Enable ResourceLoaderDebug',
37                 tooltip: 'Enable debug mode in ResourceLoader'
38         } );
40         QUnit.config.requireExpects = true;
42         /**
43          * Load TestSwarm agent
44          */
45         // Only if the current url indicates that there is a TestSwarm instance watching us
46         // (TestSwarm appends swarmURL to the test suites url it loads in iframes).
47         // Otherwise this is just a simple view of Special:JavaScriptTest/qunit directly,
48         // no point in loading inject.js in that case. Also, make sure that this instance
49         // of MediaWiki has actually been configured with the required url to that inject.js
50         // script. By default it is false.
51         if ( QUnit.urlParams.swarmURL && mw.config.get( 'QUnitTestSwarmInjectJSPath' ) ) {
52                 jQuery.getScript( QUnit.fixurl( mw.config.get( 'QUnitTestSwarmInjectJSPath' ) ) );
53         }
55         /**
56          * CompletenessTest
57          *
58          * Adds toggle checkbox to header
59          */
60         QUnit.config.urlConfig.push( {
61                 id: 'completenesstest',
62                 label: 'Run CompletenessTest',
63                 tooltip: 'Run the completeness test'
64         } );
66         /**
67          * SinonJS
68          *
69          * Glue code for nicer integration with QUnit setup/teardown
70          * Inspired by http://sinonjs.org/releases/sinon-qunit-1.0.0.js
71          * Fixes:
72          * - Work properly with asynchronous QUnit by using module setup/teardown
73          *   instead of synchronously wrapping QUnit.test.
74          */
75         sinon.assert.fail = function ( msg ) {
76                 QUnit.assert.ok( false, msg );
77         };
78         sinon.assert.pass = function ( msg ) {
79                 QUnit.assert.ok( true, msg );
80         };
81         sinon.config = {
82                 injectIntoThis: true,
83                 injectInto: null,
84                 properties: ['spy', 'stub', 'mock', 'sandbox'],
85                 // Don't fake timers by default
86                 useFakeTimers: false,
87                 useFakeServer: false
88         };
89         ( function () {
90                 var orgModule = QUnit.module;
92                 QUnit.module = function ( name, localEnv ) {
93                         localEnv = localEnv || {};
94                         orgModule( name, {
95                                 setup: function () {
96                                         var config = sinon.getConfig( sinon.config );
97                                         config.injectInto = this;
98                                         sinon.sandbox.create( config );
100                                         if ( localEnv.setup ) {
101                                                 localEnv.setup.call( this );
102                                         }
103                                 },
104                                 teardown: function () {
105                                         this.sandbox.verifyAndRestore();
107                                         if ( localEnv.teardown ) {
108                                                 localEnv.teardown.call( this );
109                                         }
110                                 }
111                         } );
112                 };
113         }() );
115         // Initiate when enabled
116         if ( QUnit.urlParams.completenesstest ) {
118                 // Return true to ignore
119                 mwTestIgnore = function ( val, tester ) {
121                         // Don't record methods of the properties of constructors,
122                         // to avoid getting into a loop (prototype.constructor.prototype..).
123                         // Since we're therefor skipping any injection for
124                         // "new mw.Foo()", manually set it to true here.
125                         if ( val instanceof mw.Map ) {
126                                 tester.methodCallTracker.Map = true;
127                                 return true;
128                         }
129                         if ( val instanceof mw.Title ) {
130                                 tester.methodCallTracker.Title = true;
131                                 return true;
132                         }
134                         // Don't record methods of the properties of a jQuery object
135                         if ( val instanceof $ ) {
136                                 return true;
137                         }
139                         return false;
140                 };
142                 mwTester = new CompletenessTest( mw, mwTestIgnore );
143         }
145         /**
146          * Test environment recommended for all QUnit test modules
147          *
148          * Whether to log environment changes to the console
149          */
150         QUnit.config.urlConfig.push( 'mwlogenv' );
152         /**
153          * Reset mw.config and others to a fresh copy of the live config for each test(),
154          * and restore it back to the live one afterwards.
155          * @param localEnv {Object} [optional]
156          * @example (see test suite at the bottom of this file)
157          * </code>
158          */
159         QUnit.newMwEnvironment = ( function () {
160                 var warn, log, liveConfig, liveMessages;
162                 liveConfig = mw.config.values;
163                 liveMessages = mw.messages.values;
165                 function suppressWarnings() {
166                         warn = mw.log.warn;
167                         mw.log.warn = $.noop;
168                 }
170                 function restoreWarnings() {
171                         if ( warn !== undefined ) {
172                                 mw.log.warn = warn;
173                                 warn = undefined;
174                         }
175                 }
177                 function freshConfigCopy( custom ) {
178                         var copy;
179                         // Tests should mock all factors that directly influence the tested code.
180                         // For backwards compatibility though we set mw.config to a fresh copy of the live
181                         // config. This way any modifications made to mw.config during the test will not
182                         // affect other tests, nor the global scope outside the test runner.
183                         // This is a shallow copy, since overriding an array or object value via "custom"
184                         // should replace it. Setting a config property means you override it, not extend it.
185                         // NOTE: It is important that we suppress warnings because extend() will also access
186                         // deprecated properties and trigger deprecation warnings from mw.log#deprecate.
187                         suppressWarnings();
188                         copy = $.extend( {}, liveConfig, custom );
189                         restoreWarnings();
191                         return copy;
192                 }
194                 function freshMessagesCopy( custom ) {
195                         return $.extend( /*deep=*/true, {}, liveMessages, custom );
196                 }
198                 log = QUnit.urlParams.mwlogenv ? mw.log : function () {};
200                 return function ( localEnv ) {
201                         localEnv = $.extend( {
202                                 // QUnit
203                                 setup: $.noop,
204                                 teardown: $.noop,
205                                 // MediaWiki
206                                 config: {},
207                                 messages: {}
208                         }, localEnv );
210                         return {
211                                 setup: function () {
212                                         log( 'MwEnvironment> SETUP    for "' + QUnit.config.current.module
213                                                 + ': ' + QUnit.config.current.testName + '"' );
215                                         // Greetings, mock environment!
216                                         mw.config.values = freshConfigCopy( localEnv.config );
217                                         mw.messages.values = freshMessagesCopy( localEnv.messages );
218                                         this.suppressWarnings = suppressWarnings;
219                                         this.restoreWarnings = restoreWarnings;
221                                         localEnv.setup.call( this );
222                                 },
224                                 teardown: function () {
225                                         log( 'MwEnvironment> TEARDOWN for "' + QUnit.config.current.module
226                                                 + ': ' + QUnit.config.current.testName + '"' );
228                                         localEnv.teardown.call( this );
230                                         // Farewell, mock environment!
231                                         mw.config.values = liveConfig;
232                                         mw.messages.values = liveMessages;
234                                         // As a convenience feature, automatically restore warnings if they're
235                                         // still suppressed by the end of the test.
236                                         restoreWarnings();
237                                 }
238                         };
239                 };
240         }() );
242         // $.when stops as soon as one fails, which makes sense in most
243         // practical scenarios, but not in a unit test where we really do
244         // need to wait until all of them are finished.
245         QUnit.whenPromisesComplete = function () {
246                 var altPromises = [];
248                 $.each( arguments, function ( i, arg ) {
249                         var alt = $.Deferred();
250                         altPromises.push( alt );
252                         // Whether this one fails or not, forwards it to
253                         // the 'done' (resolve) callback of the alternative promise.
254                         arg.always( alt.resolve );
255                 } );
257                 return $.when.apply( $, altPromises );
258         };
260         /**
261          * Recursively convert a node to a plain object representing its structure.
262          * Only considers attributes and contents (elements and text nodes).
263          * Attribute values are compared strictly and not normalised.
264          *
265          * @param {Node} node
266          * @return {Object|string} Plain JavaScript value representing the node.
267          */
268         function getDomStructure( node ) {
269                 var $node, children, processedChildren, i, len, el;
270                 $node = $( node );
271                 if ( node.nodeType === ELEMENT_NODE ) {
272                         children = $node.contents();
273                         processedChildren = [];
274                         for ( i = 0, len = children.length; i < len; i++ ) {
275                                 el = children[i];
276                                 if ( el.nodeType === ELEMENT_NODE || el.nodeType === TEXT_NODE ) {
277                                         processedChildren.push( getDomStructure( el ) );
278                                 }
279                         }
281                         return {
282                                 tagName: node.tagName,
283                                 attributes: $node.getAttrs(),
284                                 contents: processedChildren
285                         };
286                 } else {
287                         // Should be text node
288                         return $node.text();
289                 }
290         }
292         /**
293          * Gets structure of node for this HTML.
294          *
295          * @param {string} html HTML markup for one or more nodes.
296          */
297         function getHtmlStructure( html ) {
298                 var el = $( '<div>' ).append( html )[0];
299                 return getDomStructure( el );
300         }
302         /**
303          * Add-on assertion helpers
304          */
305         // Define the add-ons
306         addons = {
308                 // Expect boolean true
309                 assertTrue: function ( actual, message ) {
310                         QUnit.push( actual === true, actual, true, message );
311                 },
313                 // Expect boolean false
314                 assertFalse: function ( actual, message ) {
315                         QUnit.push( actual === false, actual, false, message );
316                 },
318                 // Expect numerical value less than X
319                 lt: function ( actual, expected, message ) {
320                         QUnit.push( actual < expected, actual, 'less than ' + expected, message );
321                 },
323                 // Expect numerical value less than or equal to X
324                 ltOrEq: function ( actual, expected, message ) {
325                         QUnit.push( actual <= expected, actual, 'less than or equal to ' + expected, message );
326                 },
328                 // Expect numerical value greater than X
329                 gt: function ( actual, expected, message ) {
330                         QUnit.push( actual > expected, actual, 'greater than ' + expected, message );
331                 },
333                 // Expect numerical value greater than or equal to X
334                 gtOrEq: function ( actual, expected, message ) {
335                         QUnit.push( actual >= expected, actual, 'greater than or equal to ' + expected, message );
336                 },
338                 /**
339                  * Asserts that two HTML strings are structurally equivalent.
340                  *
341                  * @param {string} actualHtml Actual HTML markup.
342                  * @param {string} expectedHtml Expected HTML markup
343                  * @param {string} message Assertion message.
344                  */
345                 htmlEqual: function ( actualHtml, expectedHtml, message ) {
346                         var actual = getHtmlStructure( actualHtml ),
347                                 expected = getHtmlStructure( expectedHtml );
349                         QUnit.push(
350                                 QUnit.equiv(
351                                         actual,
352                                         expected
353                                 ),
354                                 actual,
355                                 expected,
356                                 message
357                         );
358                 },
360                 /**
361                  * Asserts that two HTML strings are not structurally equivalent.
362                  *
363                  * @param {string} actualHtml Actual HTML markup.
364                  * @param {string} expectedHtml Expected HTML markup.
365                  * @param {string} message Assertion message.
366                  */
367                 notHtmlEqual: function ( actualHtml, expectedHtml, message ) {
368                         var actual = getHtmlStructure( actualHtml ),
369                                 expected = getHtmlStructure( expectedHtml );
371                         QUnit.push(
372                                 !QUnit.equiv(
373                                         actual,
374                                         expected
375                                 ),
376                                 actual,
377                                 expected,
378                                 message
379                         );
380                 }
381         };
383         $.extend( QUnit.assert, addons );
385         /**
386          * Small test suite to confirm proper functionality of the utilities and
387          * initializations defined above in this file.
388          */
389         QUnit.module( 'test.mediawiki.qunit.testrunner', QUnit.newMwEnvironment( {
390                 setup: function () {
391                         this.mwHtmlLive = mw.html;
392                         mw.html = {
393                                 escape: function () {
394                                         return 'mocked';
395                                 }
396                         };
397                 },
398                 teardown: function () {
399                         mw.html = this.mwHtmlLive;
400                 },
401                 config: {
402                         testVar: 'foo'
403                 },
404                 messages: {
405                         testMsg: 'Foo.'
406                 }
407         } ) );
409         QUnit.test( 'Setup', 3, function ( assert ) {
410                 assert.equal( mw.html.escape( 'foo' ), 'mocked', 'setup() callback was ran.' );
411                 assert.equal( mw.config.get( 'testVar' ), 'foo', 'config object applied' );
412                 assert.equal( mw.messages.get( 'testMsg' ), 'Foo.', 'messages object applied' );
414                 mw.config.set( 'testVar', 'bar' );
415                 mw.messages.set( 'testMsg', 'Bar.' );
416         } );
418         QUnit.test( 'Teardown', 2, function ( assert ) {
419                 assert.equal( mw.config.get( 'testVar' ), 'foo', 'config object restored and re-applied after test()' );
420                 assert.equal( mw.messages.get( 'testMsg' ), 'Foo.', 'messages object restored and re-applied after test()' );
421         } );
423         QUnit.test( 'Loader status', 2, function ( assert ) {
424                 var i, len, state,
425                         modules = mw.loader.getModuleNames(),
426                         error = [],
427                         missing = [];
429                 for ( i = 0, len = modules.length; i < len; i++ ) {
430                         state = mw.loader.getState( modules[i] );
431                         if ( state === 'error' ) {
432                                 error.push( modules[i] );
433                         } else if ( state === 'missing' ) {
434                                 missing.push( modules[i] );
435                         }
436                 }
438                 assert.deepEqual( error, [], 'Modules in error state' );
439                 assert.deepEqual( missing, [], 'Modules in missing state' );
440         } );
442         QUnit.test( 'htmlEqual', 8, function ( assert ) {
443                 assert.htmlEqual(
444                         '<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>',
445                         '<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>',
446                         'Attribute order, spacing and quotation marks (equal)'
447                 );
449                 assert.notHtmlEqual(
450                         '<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>',
451                         '<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>',
452                         'Attribute order, spacing and quotation marks (not equal)'
453                 );
455                 assert.htmlEqual(
456                         '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
457                         '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
458                         'Multiple root nodes (equal)'
459                 );
461                 assert.notHtmlEqual(
462                         '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
463                         '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="important" >Last</label><input id="lastname" />',
464                         'Multiple root nodes (not equal, last label node is different)'
465                 );
467                 assert.htmlEqual(
468                         'fo&quot;o<br/>b&gt;ar',
469                         'fo"o<br/>b>ar',
470                         'Extra escaping is equal'
471                 );
472                 assert.notHtmlEqual(
473                         'foo&lt;br/&gt;bar',
474                         'foo<br/>bar',
475                         'Text escaping (not equal)'
476                 );
478                 assert.htmlEqual(
479                         'foo<a href="http://example.com">example</a>bar',
480                         'foo<a href="http://example.com">example</a>bar',
481                         'Outer text nodes are compared (equal)'
482                 );
484                 assert.notHtmlEqual(
485                         'foo<a href="http://example.com">example</a>bar',
486                         'foo<a href="http://example.com">example</a>quux',
487                         'Outer text nodes are compared (last text node different)'
488                 );
490         } );
492         QUnit.module( 'test.mediawiki.qunit.testrunner-after', QUnit.newMwEnvironment() );
494         QUnit.test( 'Teardown', 3, function ( assert ) {
495                 assert.equal( mw.html.escape( '<' ), '&lt;', 'teardown() callback was ran.' );
496                 assert.equal( mw.config.get( 'testVar' ), null, 'config object restored to live in next module()' );
497                 assert.equal( mw.messages.get( 'testMsg' ), null, 'messages object restored to live in next module()' );
498         } );
500 }( jQuery, mediaWiki, QUnit ) );