Localisation updates from http://translatewiki.net.
[mediawiki.git] / tests / qunit / suites / resources / mediawiki / mediawiki.test.js
blob2f115215afddea1d3b725ace319621b15a985fb9
1 module( 'mediawiki', QUnit.newMwEnvironment() );
3 test( '-- Initial check', function() {
4         expect(8);
6         ok( window.jQuery, 'jQuery defined' );
7         ok( window.$, '$j defined' );
8         ok( window.$j, '$j defined' );
9         strictEqual( window.$, window.jQuery, '$ alias to jQuery' );
10         strictEqual( window.$j, window.jQuery, '$j alias to jQuery' );
12         ok( window.mediaWiki, 'mediaWiki defined' );
13         ok( window.mw, 'mw defined' );
14         strictEqual( window.mw, window.mediaWiki, 'mw alias to mediaWiki' );
15 });
17 test( 'mw.Map', function() {
18         var arry, conf, funky, globalConf, nummy, someValues;
19         expect(17);
21         ok( mw.Map, 'mw.Map defined' );
23         conf = new mw.Map();
24         // Dummy variables
25         funky = function () {};
26         arry = [];
27         nummy = 7;
29         // Tests for input validation
30         strictEqual( conf.get( 'inexistantKey' ), null, 'Map.get returns null if selection was a string and the key was not found' );
31         strictEqual( conf.set( 'myKey', 'myValue' ), true, 'Map.set returns boolean true if a value was set for a valid key string' );
32         strictEqual( conf.set( funky, 'Funky' ), false, 'Map.set returns boolean false if key was invalid (Function)' );
33         strictEqual( conf.set( arry, 'Arry' ), false, 'Map.set returns boolean false if key was invalid (Array)' );
34         strictEqual( conf.set( nummy, 'Nummy' ), false, 'Map.set returns boolean false if key was invalid (Number)' );
35         equal( conf.get( 'myKey' ), 'myValue', 'Map.get returns a single value value correctly' );
36         strictEqual( conf.get( nummy ), null, 'Map.get ruturns null if selection was invalid (Number)' );
37         strictEqual( conf.get( funky ), null, 'Map.get ruturns null if selection was invalid (Function)' );
39         // Multiple values at once
40         someValues = {
41                 'foo': 'bar',
42                 'lorem': 'ipsum',
43                 'MediaWiki': true
44         };
45         strictEqual( conf.set( someValues ), true, 'Map.set returns boolean true if multiple values were set by passing an object' );
46         deepEqual( conf.get( ['foo', 'lorem'] ), {
47                 'foo': 'bar',
48                 'lorem': 'ipsum'
49         }, 'Map.get returns multiple values correctly as an object' );
51         deepEqual( conf.get( ['foo', 'notExist'] ), {
52                 'foo': 'bar',
53                 'notExist': null
54         }, 'Map.get return includes keys that were not found as null values' );
56         strictEqual( conf.exists( 'foo' ), true, 'Map.exists returns boolean true if a key exists' );
57         strictEqual( conf.exists( 'notExist' ), false, 'Map.exists returns boolean false if a key does not exists' );
59         // Interacting with globals and accessing the values object
60         strictEqual( conf.get(), conf.values, 'Map.get returns the entire values object by reference (if called without arguments)' );
62         conf.set( 'globalMapChecker', 'Hi' );
64         ok( false === 'globalMapChecker' in window, 'new mw.Map did not store its values in the global window object by default' );
66         globalConf = new mw.Map( true );
67         globalConf.set( 'anotherGlobalMapChecker', 'Hello' );
69         ok( 'anotherGlobalMapChecker' in window, 'new mw.Map( true ) did store its values in the global window object' );
71         // Whitelist this global variable for QUnit's 'noglobal' mode
72         if ( QUnit.config.noglobals ) {
73                 QUnit.config.pollution.push( 'anotherGlobalMapChecker' );
74         }
75 });
77 test( 'mw.config', function() {
78         expect(1);
80         ok( mw.config instanceof mw.Map, 'mw.config instance of mw.Map' );
81 });
83 test( 'mw.message & mw.messages', function() {
84         var goodbye, hello, pluralMessage;
85         expect(20);
87         ok( mw.messages, 'messages defined' );
88         ok( mw.messages instanceof mw.Map, 'mw.messages instance of mw.Map' );
89         ok( mw.messages.set( 'hello', 'Hello <b>awesome</b> world' ), 'mw.messages.set: Register' );
91         hello = mw.message( 'hello' );
93         equal( hello.format, 'plain', 'Message property "format" defaults to "plain"' );
94         strictEqual( hello.map, mw.messages, 'Message property "map" defaults to the global instance in mw.messages' );
95         equal( hello.key, 'hello', 'Message property "key" (currect key)' );
96         deepEqual( hello.parameters, [], 'Message property "parameters" defaults to an empty array' );
98         // Todo
99         ok( hello.params, 'Message prototype "params"' );
101         hello.format = 'plain';
102         equal( hello.toString(), 'Hello <b>awesome</b> world', 'Message.toString returns the message as a string with the current "format"' );
104         equal( hello.escaped(), 'Hello &lt;b&gt;awesome&lt;/b&gt; world', 'Message.escaped returns the escaped message' );
105         equal( hello.format, 'escaped', 'Message.escaped correctly updated the "format" property' );
107         hello.parse();
108         equal( hello.format, 'parse', 'Message.parse correctly updated the "format" property' );
110         hello.plain();
111         equal( hello.format, 'plain', 'Message.plain correctly updated the "format" property' );
113         strictEqual( hello.exists(), true, 'Message.exists returns true for existing messages' );
115         goodbye = mw.message( 'goodbye' );
116         strictEqual( goodbye.exists(), false, 'Message.exists returns false for nonexistent messages' );
118         equal( goodbye.plain(), '<goodbye>', 'Message.toString returns plain <key> if format is "plain" and key does not exist' );
119         // bug 30684
120         equal( goodbye.escaped(), '&lt;goodbye&gt;', 'Message.toString returns properly escaped &lt;key&gt; if format is "escaped" and key does not exist' );
122         ok( mw.messages.set( 'pluraltestmsg', 'There {{PLURAL:$1|is|are}} $1 {{PLURAL:$1|result|results}}' ), 'mw.messages.set: Register' );
123         pluralMessage = mw.message( 'pluraltestmsg' , 6 );
124         equal( pluralMessage.plain(), 'There are 6 results', 'plural get resolved when format is plain' );
125         equal( pluralMessage.parse(), 'There are 6 results', 'plural get resolved when format is parse' );
129 test( 'mw.msg', function() {
130         expect(11);
132         ok( mw.messages.set( 'hello', 'Hello <b>awesome</b> world' ), 'mw.messages.set: Register' );
133         equal( mw.msg( 'hello' ), 'Hello <b>awesome</b> world', 'Gets message with default options (existing message)' );
134         equal( mw.msg( 'goodbye' ), '<goodbye>', 'Gets message with default options (nonexistent message)' );
136         ok( mw.messages.set( 'plural-item' , 'Found $1 {{PLURAL:$1|item|items}}' ) );
137         equal( mw.msg( 'plural-item', 5 ), 'Found 5 items', 'Apply plural for count 5' );
138         equal( mw.msg( 'plural-item', 0 ), 'Found 0 items', 'Apply plural for count 0' );
139         equal( mw.msg( 'plural-item', 1 ), 'Found 1 item', 'Apply plural for count 1' );
141         ok( mw.messages.set('gender-plural-msg' , '{{GENDER:$1|he|she|they}} {{PLURAL:$2|is|are}} awesome' ) );
142         equal( mw.msg( 'gender-plural-msg', 'male', 1 ), 'he is awesome', 'Gender test for male, plural count 1' );
143         equal( mw.msg( 'gender-plural-msg', 'female', '1' ), 'she is awesome', 'Gender test for female, plural count 1' );
144         equal( mw.msg( 'gender-plural-msg', 'unknown', 10 ), 'they are awesome', 'Gender test for neutral, plural count 10' );
148 test( 'mw.loader', function() {
149         var isAwesomeDone;
150         expect(2);
152         // Async ahead
153         stop();
155         mw.loader.testCallback = function () {
156                 start();
157                 strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined');
158                 isAwesomeDone = true;
159         };
161         mw.loader.implement( 'test.callback', [QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' )], {}, {} );
163         mw.loader.using( 'test.callback', function () {
165                 // /sample/awesome.js declares the "mw.loader.testCallback" function
166                 // which contains a call to start() and ok()
167                 strictEqual( isAwesomeDone, true, "test.callback module should've caused isAwesomeDone to be true" );
168                 delete mw.loader.testCallback;
170         }, function () {
171                 start();
172                 ok( false, 'Error callback fired while loader.using "test.callback" module' );
173         });
176 test( 'mw.loader.implement', function () {
177         var isJsExecuted, $element, styleTestUrl;
178         expect(5);
180         // Async ahead
181         stop();
183         styleTestUrl = QUnit.fixurl(
184                 mw.config.get( 'wgScriptPath' )
185                 + '/tests/qunit/data/styleTest.css.php?'
186                 + $.param({
187                         selector: '.mw-test-loaderimplement',
188                         prop: 'float',
189                         val: 'right'
190                 })
191         );
193         mw.loader.implement(
194                 'test.implement',
195                 function () {
196                         var styleTestTimeout, styleTestStart, styleTestSince;
198                         strictEqual( isJsExecuted, undefined, 'javascript not executed multiple times' );
199                         isJsExecuted = true;
201                         equal( mw.loader.getState( 'test.implement' ), 'loaded', 'module state is "loaded" while implement() is executing javascript' );
203                         $element = $( '<div class="mw-test-loaderimplement">Foo bar</div>' ).appendTo( '#qunit-fixture' );
205                         equal( mw.msg( 'test-foobar' ), 'Hello Foobar, $1!', 'Messages are loaded before javascript execution' );
207                         // The @import test. This is, in a way, also an open bug for ResourceLoader
208                         // ("execute js after styles are loaded"), but browsers don't offer a way to
209                         // get a callback from when a stylesheet is loaded (that is, including any
210                         // @import rules inside).
211                         // To work around this, we'll have a little time loop to check if the styles
212                         // apply.
213                         // Note: This test originally used new Image() and onerror to get a callback
214                         // when the url is loaded, but that is fragile since it doesn't monitor the
215                         // same request as the css @import, and Safari 4 has issues with
216                         // onerror/onload not being fired at all in weird cases like this.
218                         styleTestTimeout = QUnit.config.testTimeout || 5000; // milliseconds
220                         function isCssImportApplied() {
221                                 return $element.css( 'float' ) === 'right';
222                         }
224                         function styleTestLoop() {
225                                 styleTestSince = new Date().getTime() - styleTestStart;
226                                 // If it is passing or if we timed out, run the real test and stop the loop
227                                 if ( isCssImportApplied() || styleTestSince > styleTestTimeout ) {
228                                         equal( $element.css( 'float' ), 'right',
229                                                 'CSS stylesheet via @import was applied (after ' + styleTestSince + 'ms) (bug 34669). ("float: right")'
230                                         );
232                                         equal( $element.css( 'text-align' ),'center',
233                                                 'CSS styles after the @import are working ("text-align: center")'
234                                         );
236                                         // Async done
237                                         start();
239                                         return;
240                                 }
241                                 // Otherwise, keep polling
242                                 setTimeout( styleTestLoop, 100 );
243                         }
245                         // Start the loop
246                         styleTestStart = new Date().getTime();
247                         styleTestLoop();
248                 },
249                 {
250                         "all": "@import url('"
251                                 + styleTestUrl
252                                 + "');\n"
253                                 + '.mw-test-loaderimplement { text-align: center; }'
254                 },
255                 {
256                         "test-foobar": "Hello Foobar, $1!"
257                 }
258         );
260         mw.loader.load( 'test.implement' );
264 test( 'mw.loader erroneous indirect dependency', function() {
265         expect( 3 );
266         mw.loader.register( [
267                 ['test.module1', '0'],
268                 ['test.module2', '0', ['test.module1']],
269                 ['test.module3', '0', ['test.module2']]
270         ] );
271         mw.loader.implement( 'test.module1', function() { throw new Error( 'expected' ); }, {}, {} );
272         strictEqual( mw.loader.getState( 'test.module1' ), 'error', 'Expected "error" state for test.module1' );
273         strictEqual( mw.loader.getState( 'test.module2' ), 'error', 'Expected "error" state for test.module2' );
274         strictEqual( mw.loader.getState( 'test.module3' ), 'error', 'Expected "error" state for test.module3' );
275 } );
277 test( 'mw.loader out-of-order implementation', function() {
278         expect( 9 );
279         mw.loader.register( [
280                 ['test.module4', '0'],
281                 ['test.module5', '0', ['test.module4']],
282                 ['test.module6', '0', ['test.module5']]
283         ] );
284         mw.loader.implement( 'test.module4', function() {}, {}, {} );
285         strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
286         strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' );
287         strictEqual( mw.loader.getState( 'test.module6' ), 'registered', 'Expected "registered" state for test.module6' );
288         mw.loader.implement( 'test.module6', function() {}, {}, {} );
289         strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
290         strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' );
291         strictEqual( mw.loader.getState( 'test.module6' ), 'loaded', 'Expected "loaded" state for test.module6' );
292         mw.loader.implement( 'test.module5', function() {}, {}, {} );
293         strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
294         strictEqual( mw.loader.getState( 'test.module5' ), 'ready', 'Expected "ready" state for test.module5' );
295         strictEqual( mw.loader.getState( 'test.module6' ), 'ready', 'Expected "ready" state for test.module6' );
296 } );
298 test( 'mw.loader missing dependency', function() {
299         expect( 13 );
300         mw.loader.register( [
301                 ['test.module7', '0'],
302                 ['test.module8', '0', ['test.module7']],
303                 ['test.module9', '0', ['test.module8']]
304         ] );
305         mw.loader.implement( 'test.module8', function() {}, {}, {} );
306         strictEqual( mw.loader.getState( 'test.module7' ), 'registered', 'Expected "registered" state for test.module7' );
307         strictEqual( mw.loader.getState( 'test.module8' ), 'loaded', 'Expected "loaded" state for test.module8' );
308         strictEqual( mw.loader.getState( 'test.module9' ), 'registered', 'Expected "registered" state for test.module9' );
309         mw.loader.state( 'test.module7', 'missing' );
310         strictEqual( mw.loader.getState( 'test.module7' ), 'missing', 'Expected "missing" state for test.module7' );
311         strictEqual( mw.loader.getState( 'test.module8' ), 'error', 'Expected "error" state for test.module8' );
312         strictEqual( mw.loader.getState( 'test.module9' ), 'error', 'Expected "error" state for test.module9' );
313         mw.loader.implement( 'test.module9', function() {}, {}, {} );
314         strictEqual( mw.loader.getState( 'test.module7' ), 'missing', 'Expected "missing" state for test.module7' );
315         strictEqual( mw.loader.getState( 'test.module8' ), 'error', 'Expected "error" state for test.module8' );
316         strictEqual( mw.loader.getState( 'test.module9' ), 'error', 'Expected "error" state for test.module9' );
317         mw.loader.using(
318                 ['test.module7'],
319                 function() {
320                         ok( false, "Success fired despite missing dependency" );
321                         ok( true , "QUnit expected() count dummy" );
322                 },
323                 function( e, dependencies ) {
324                         strictEqual( $.isArray( dependencies ), true, 'Expected array of dependencies' );
325                         deepEqual( dependencies, ['test.module7'], 'Error callback called with module test.module7' );
326                 }
327         );
328         mw.loader.using(
329                 ['test.module9'],
330                 function() {
331                         ok( false, "Success fired despite missing dependency" );
332                         ok( true , "QUnit expected() count dummy" );
333                 },
334                 function( e, dependencies ) {
335                         strictEqual( $.isArray( dependencies ), true, 'Expected array of dependencies' );
336                         dependencies.sort();
337                         deepEqual(
338                                 dependencies,
339                                 ['test.module7', 'test.module8', 'test.module9'],
340                                 'Error callback called with all three modules as dependencies'
341                         );
342                 }
343         );
344 } );
346 test( 'mw.loader real missing dependency', function() {
347         expect( 6 );
349         mw.loader.addSource(
350                 'test',
351                 {'loadScript' : QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/testloader.php' )}
352         );
353         mw.loader.register( [
354                 ['test.missing', '0', [], null, 'test'], ['test.missing2', '0', [], null, 'test'],
355                 ['test.use_missing', '0', ['test.missing'], null, 'test'],
356                 ['test.use_missing2', '0', ['test.missing2'], null, 'test']
357         ] );
359         stop();
360         // Asynch ahead
362         mw.loader.load( ['test.use_missing'] );
364         function verifyModuleStates() {
365                 strictEqual( mw.loader.getState( 'test.missing' ), 'missing', 'Module "test.missing" must have state "missing"' );
366                 strictEqual( mw.loader.getState( 'test.missing2' ), 'missing', 'Module "test.missing2" must have state "missing"' );
367                 strictEqual( mw.loader.getState( 'test.use_missing' ), 'error', 'Module "test.use_missing" must have state "error"' );
368                 strictEqual( mw.loader.getState( 'test.use_missing2' ), 'error', 'Module "test.use_missing2" must have state "error"' );
369         }
371         mw.loader.using( ['test.use_missing2'],
372                 function() {
373                         start();
374                         ok( false, "Success called wrongly." );
375                         ok( true , "QUnit expected() count dummy" );
376                         verifyModuleStates();
377                 },
378                 function( e, dependencies ) {
379                         start();
380                         ok( true, "Error handler called correctly." );
381                         deepEqual( dependencies, ['test.missing2'], "Dependencies correct." );
382                         verifyModuleStates();
383                 }
384         );
385 } );
387 test( 'mw.loader bug29107' , function () {
388         expect(2);
390         // Message doesn't exist already
391         ok( !mw.messages.exists( 'bug29107' ) );
393         // Async! Failure in this test may lead to neither the success nor error callbacks getting called.
394         // Due to QUnit's timeout feauture we won't hang here forever if this happends.
395         stop();
397         mw.loader.implement( 'bug29107.messages-only', [], {}, {'bug29107': 'loaded'} );
398         mw.loader.using( 'bug29107.messages-only', function() {
399                 start();
400                 ok( mw.messages.exists( 'bug29107' ), 'Bug 29107: messages-only module should implement ok' );
401         }, function() {
402                 start();
403                 ok( false, 'Error callback fired while implementing "bug29107.messages-only" module' );
404         });
407 test( 'mw.loader.bug30825', function() {
408         // This bug was actually already fixed in 1.18 and later when discovered in 1.17.
409         // Test is for regressions!
411         expect(2);
413         // Forge an URL to the test callback script
414         var target = QUnit.fixurl(
415                 mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/qunitOkCall.js'
416         );
418         // Confirm that mw.loader.load() works with protocol-relative URLs
419         target = target.replace( /https?:/, '' );
421         equal( target.substr( 0, 2 ), '//',
422                 'URL must be relative to test relative URLs!'
423         );
425         // Async!
426         stop();
427         mw.loader.load( target );
430 test( 'mw.html', function () {
431         expect(13);
433         raises( function () {
434                 mw.html.escape();
435         }, TypeError, 'html.escape throws a TypeError if argument given is not a string' );
437         equal( mw.html.escape( '<mw awesome="awesome" value=\'test\' />' ),
438                 '&lt;mw awesome=&quot;awesome&quot; value=&#039;test&#039; /&gt;', 'escape() escapes special characters to html entities' );
440         equal( mw.html.element(),
441                 '<undefined/>', 'element() always returns a valid html string (even without arguments)' );
443         equal( mw.html.element( 'div' ), '<div/>', 'element() Plain DIV (simple)' );
445         equal( mw.html.element( 'div', {}, '' ), '<div></div>', 'element() Basic DIV (simple)' );
447         equal(
448                 mw.html.element(
449                         'div', {
450                                 id: 'foobar'
451                         }
452                 ),
453                 '<div id="foobar"/>',
454                 'html.element DIV (attribs)' );
456         equal( mw.html.element( 'p', null, 12 ), '<p>12</p>', 'Numbers are valid content and should be casted to a string' );
458         equal( mw.html.element( 'p', { title: 12 }, '' ), '<p title="12"></p>', 'Numbers are valid attribute values' );
460         // Example from https://www.mediawiki.org/wiki/ResourceLoader/Default_modules#mediaWiki.html
461         equal(
462                 mw.html.element(
463                         'div',
464                         {},
465                         new mw.html.Raw(
466                                 mw.html.element( 'img', { src: '<' } )
467                         )
468                 ),
469                 '<div><img src="&lt;"/></div>',
470                 'Raw inclusion of another element'
471         );
473         equal(
474                 mw.html.element(
475                         'option', {
476                                 selected: true
477                         }, 'Foo'
478                 ),
479                 '<option selected="selected">Foo</option>',
480                 'Attributes may have boolean values. True copies the attribute name to the value.'
481         );
483         equal(
484                 mw.html.element(
485                         'option', {
486                                 value: 'foo',
487                                 selected: false
488                         }, 'Foo'
489                 ),
490                 '<option value="foo">Foo</option>',
491                 'Attributes may have boolean values. False keeps the attribute from output.'
492         );
494         equal( mw.html.element( 'div',
495                         null, 'a' ),
496                 '<div>a</div>',
497                 'html.element DIV (content)' );
499         equal( mw.html.element( 'a',
500                         { href: 'http://mediawiki.org/w/index.php?title=RL&action=history' }, 'a' ),
501                 '<a href="http://mediawiki.org/w/index.php?title=RL&amp;action=history">a</a>',
502                 'html.element DIV (attribs + content)' );