Use local context to get messages
[mediawiki.git] / tests / jasmine / spec / mediawiki.jqueryMsg.spec.js
blob46dcaa8032f93c05a41f18956dbd125cb5c2bcd5
1 /* spec for language & message behaviour in MediaWiki */
3 mw.messages.set( {
4         "en_empty": "",
5         "en_simple": "Simple message",
6         "en_replace": "Simple $1 replacement",
7         "en_replace2": "Simple $1 $2 replacements",
8         "en_link": "Simple [http://example.com link to example].",
9         "en_link_replace": "Complex [$1 $2] behaviour.",
10         "en_simple_magic": "Simple {{ALOHOMORA}} message",
11         "en_undelete_short": "Undelete {{PLURAL:$1|one edit|$1 edits}}",
12         "en_undelete_empty_param": "Undelete{{PLURAL:$1|| multiple edits}}",
13         "en_category-subcat-count": "{{PLURAL:$2|This category has only the following subcategory.|This category has the following {{PLURAL:$1|subcategory|$1 subcategories}}, out of $2 total.}}",
14         "en_escape0": "Escape \\to fantasy island",
15         "en_escape1": "I had \\$2.50 in my pocket",
16         "en_escape2": "I had {{PLURAL:$1|the absolute \\|$1\\| which came out to \\$3.00 in my C:\\\\drive| some stuff}}",
17         "en_fail": "This should fail to {{parse",
18         "en_fail_magic": "There is no such magic word as {{SIETNAME}}",
19         "en_evil": "This has <script type='text/javascript'>window.en_evil = true;</script> tags"
20 } );
22 /**
23  * Tests
24  */
25 ( function( mw, $, undefined ) {
27         describe( "mediaWiki.jqueryMsg", function() {
28                 
29                 describe( "basic message functionality", function() {
31                         it( "should return identity for empty string", function() {
32                                 var parser = new mw.jqueryMsg.parser();
33                                 expect( parser.parse( 'en_empty' ).html() ).toEqual( '' );
34                         } );
37                         it( "should return identity for simple string", function() {
38                                 var parser = new mw.jqueryMsg.parser();
39                                 expect( parser.parse( 'en_simple' ).html() ).toEqual( 'Simple message' );
40                         } );
42                 } );
44                 describe( "escaping", function() {
46                         it ( "should handle simple escaping", function() {
47                                 var parser = new mw.jqueryMsg.parser();
48                                 expect( parser.parse( 'en_escape0' ).html() ).toEqual( 'Escape to fantasy island' );
49                         } );
51                         it ( "should escape dollar signs found in ordinary text when backslashed", function() {
52                                 var parser = new mw.jqueryMsg.parser();
53                                 expect( parser.parse( 'en_escape1' ).html() ).toEqual( 'I had $2.50 in my pocket' );
54                         } );
56                         it ( "should handle a complicated escaping case, including escaped pipe chars in template args", function() {
57                                 var parser = new mw.jqueryMsg.parser();
58                                 expect( parser.parse( 'en_escape2', [ 1 ] ).html() ).toEqual( 'I had the absolute |1| which came out to $3.00 in my C:\\drive' );
59                         } );
61                 } );
63                 describe( "replacing", function() {
65                         it ( "should handle simple replacing", function() {
66                                 var parser = new mw.jqueryMsg.parser();
67                                 expect( parser.parse( 'en_replace', [ 'foo' ] ).html() ).toEqual( 'Simple foo replacement' );
68                         } );
70                         it ( "should return $n if replacement not there", function() {
71                                 var parser = new mw.jqueryMsg.parser();
72                                 expect( parser.parse( 'en_replace', [] ).html() ).toEqual( 'Simple $1 replacement' );
73                                 expect( parser.parse( 'en_replace2', [ 'bar' ] ).html() ).toEqual( 'Simple bar $2 replacements' );
74                         } );
76                 } );
78                 describe( "linking", function() {
80                         it ( "should handle a simple link", function() {
81                                 var parser = new mw.jqueryMsg.parser();
82                                 var parsed = parser.parse( 'en_link' );
83                                 var contents = parsed.contents();
84                                 expect( contents.length ).toEqual( 3 );
85                                 expect( contents[0].nodeName ).toEqual( '#text' );
86                                 expect( contents[0].nodeValue ).toEqual( 'Simple ' );
87                                 expect( contents[1].nodeName ).toEqual( 'A' );
88                                 expect( contents[1].getAttribute( 'href' ) ).toEqual( 'http://example.com' );
89                                 expect( contents[1].childNodes[0].nodeValue ).toEqual( 'link to example' );
90                                 expect( contents[2].nodeName ).toEqual( '#text' );
91                                 expect( contents[2].nodeValue ).toEqual( '.' );
92                         } );
94                         it ( "should replace a URL into a link", function() {
95                                 var parser = new mw.jqueryMsg.parser();
96                                 var parsed = parser.parse( 'en_link_replace', [ 'http://example.com/foo', 'linking' ] );
97                                 var contents = parsed.contents();
98                                 expect( contents.length ).toEqual( 3 );
99                                 expect( contents[0].nodeName ).toEqual( '#text' );
100                                 expect( contents[0].nodeValue ).toEqual( 'Complex ' );
101                                 expect( contents[1].nodeName ).toEqual( 'A' );
102                                 expect( contents[1].getAttribute( 'href' ) ).toEqual( 'http://example.com/foo' );
103                                 expect( contents[1].childNodes[0].nodeValue ).toEqual( 'linking' );
104                                 expect( contents[2].nodeName ).toEqual( '#text' );
105                                 expect( contents[2].nodeValue ).toEqual( ' behaviour.' );
106                         } );
108                         it ( "should bind a click handler into a link", function() {
109                                 var parser = new mw.jqueryMsg.parser();
110                                 var clicked = false;
111                                 var click = function() { clicked = true; };
112                                 var parsed = parser.parse( 'en_link_replace', [ click, 'linking' ] );
113                                 var contents = parsed.contents();
114                                 expect( contents.length ).toEqual( 3 );
115                                 expect( contents[0].nodeName ).toEqual( '#text' );
116                                 expect( contents[0].nodeValue ).toEqual( 'Complex ' );
117                                 expect( contents[1].nodeName ).toEqual( 'A' );
118                                 expect( contents[1].getAttribute( 'href' ) ).toEqual( '#' );
119                                 expect( contents[1].childNodes[0].nodeValue ).toEqual( 'linking' );
120                                 expect( contents[2].nodeName ).toEqual( '#text' );
121                                 expect( contents[2].nodeValue ).toEqual( ' behaviour.' );
122                                 // determining bindings is hard in IE
123                                 var anchor = parsed.find( 'a' );
124                                 if ( ( $.browser.mozilla || $.browser.webkit ) && anchor.click ) {
125                                         expect( clicked ).toEqual( false );
126                                         anchor.click(); 
127                                         expect( clicked ).toEqual( true );
128                                 }
129                         } );
131                         it ( "should wrap a jquery arg around link contents -- even another element", function() {
132                                 var parser = new mw.jqueryMsg.parser();
133                                 var clicked = false;
134                                 var click = function() { clicked = true; };
135                                 var button = $( '<button>' ).click( click );
136                                 var parsed = parser.parse( 'en_link_replace', [ button, 'buttoning' ] );
137                                 var contents = parsed.contents();
138                                 expect( contents.length ).toEqual( 3 );
139                                 expect( contents[0].nodeName ).toEqual( '#text' );
140                                 expect( contents[0].nodeValue ).toEqual( 'Complex ' );
141                                 expect( contents[1].nodeName ).toEqual( 'BUTTON' );
142                                 expect( contents[1].childNodes[0].nodeValue ).toEqual( 'buttoning' );
143                                 expect( contents[2].nodeName ).toEqual( '#text' );
144                                 expect( contents[2].nodeValue ).toEqual( ' behaviour.' );
145                                 // determining bindings is hard in IE
146                                 if ( ( $.browser.mozilla || $.browser.webkit ) && button.click ) {
147                                         expect( clicked ).toEqual( false );
148                                         parsed.find( 'button' ).click();
149                                         expect( clicked ).toEqual( true );
150                                 }
151                         } );
154                 } );
157                 describe( "magic keywords", function() {
158                         it( "should substitute magic keywords", function() {
159                                 var options = {
160                                         magic: { 
161                                                 'alohomora' : 'open'
162                                         }
163                                 };
164                                 var parser = new mw.jqueryMsg.parser( options );
165                                 expect( parser.parse( 'en_simple_magic' ).html() ).toEqual( 'Simple open message' );
166                         } );
167                 } );
168                 
169                 describe( "error conditions", function() {
170                         it( "should return non-existent key in square brackets", function() {
171                                 var parser = new mw.jqueryMsg.parser();
172                                 expect( parser.parse( 'en_does_not_exist' ).html() ).toEqual( '[en_does_not_exist]' );
173                         } );
176                         it( "should fail to parse", function() {
177                                 var parser = new mw.jqueryMsg.parser();
178                                 expect( function() { parser.parse( 'en_fail' ); } ).toThrow( 
179                                         'Parse error at position 20 in input: This should fail to {{parse'
180                                 );
181                         } );
182                 } );
184                 describe( "empty parameters", function() {
185                         it( "should deal with empty parameters", function() {
186                                 var parser = new mw.jqueryMsg.parser();
187                                 var ast = parser.getAst( 'en_undelete_empty_param' );
188                                 expect( parser.parse( 'en_undelete_empty_param', [ 1 ] ).html() ).toEqual( 'Undelete' );
189                                 expect( parser.parse( 'en_undelete_empty_param', [ 3 ] ).html() ).toEqual( 'Undelete multiple edits' );
191                         } );
192                 } );
194                 describe( "easy message interface functions", function() {
195                         it( "should allow a global that returns strings", function() {
196                                 var gM = mw.jqueryMsg.getMessageFunction();
197                                 // passing this through jQuery and back to string, because browsers may have subtle differences, like the case of tag names.
198                                 // a surrounding <SPAN> is needed for html() to work right
199                                 var expectedHtml = $( '<span>Complex <a href="http://example.com/foo">linking</a> behaviour.</span>' ).html();
200                                 var result = gM( 'en_link_replace', 'http://example.com/foo', 'linking' );
201                                 expect( typeof result ).toEqual( 'string' );
202                                 expect( result ).toEqual( expectedHtml );
203                         } );
205                         it( "should allow a jQuery plugin that appends to nodes", function() {
206                                 $.fn.msg = mw.jqueryMsg.getPlugin();
207                                 var $div = $( '<div>' ).append( $( '<p>' ).addClass( 'foo' ) );
208                                 var clicked = false;
209                                 var $button = $( '<button>' ).click( function() { clicked = true; } );
210                                 $div.find( '.foo' ).msg( 'en_link_replace', $button, 'buttoning' );
211                                 // passing this through jQuery and back to string, because browsers may have subtle differences, like the case of tag names.
212                                 // a surrounding <SPAN> is needed for html() to work right
213                                 var expectedHtml = $( '<span>Complex <button>buttoning</button> behaviour.</span>' ).html();
214                                 var createdHtml = $div.find( '.foo' ).html();
215                                 // it is hard to test for clicks with IE; also it inserts or removes spaces around nodes when creating HTML tags, depending on their type.
216                                 // so need to check the strings stripped of spaces.
217                                 if ( ( $.browser.mozilla || $.browser.webkit ) && $button.click ) {
218                                         expect( createdHtml ).toEqual( expectedHtml );
219                                         $div.find( 'button ').click();
220                                         expect( clicked ).toEqual( true );
221                                 } else if ( $.browser.ie ) {
222                                         expect( createdHtml.replace( /\s/, '' ) ).toEqual( expectedHtml.replace( /\s/, '' ) );
223                                 }
224                                 delete $.fn.msg;
225                         } );
227                         it( "jQuery plugin should escape incoming string arguments", function() {
228                                 $.fn.msg = mw.jqueryMsg.getPlugin();
229                                 var $div = $( '<div>' ).addClass( 'foo' );
230                                 $div.msg( 'en_replace', '<p>x</p>' ); // looks like HTML, but as a string, should be escaped.
231                                 // passing this through jQuery and back to string, because browsers may have subtle differences, like the case of tag names.
232                                 var expectedHtml = $( '<div class="foo">Simple &lt;p&gt;x&lt;/p&gt; replacement</div>' ).html();
233                                 var createdHtml = $div.html();
234                                 expect( expectedHtml ).toEqual( createdHtml );
235                                 delete $.fn.msg;
236                         } );
239                         it( "jQuery plugin should never execute scripts", function() {
240                                 window.en_evil = false;
241                                 $.fn.msg = mw.jqueryMsg.getPlugin();
242                                 var $div = $( '<div>' );
243                                 $div.msg( 'en_evil' );
244                                 expect( window.en_evil ).toEqual( false );
245                                 delete $.fn.msg;
246                         } );
249                         // n.b. this passes because jQuery already seems to strip scripts away; however, it still executes them if they are appended to any element.
250                         it( "jQuery plugin should never emit scripts", function() {
251                                 $.fn.msg = mw.jqueryMsg.getPlugin();
252                                 var $div = $( '<div>' );
253                                 $div.msg( 'en_evil' );
254                                 // passing this through jQuery and back to string, because browsers may have subtle differences, like the case of tag names.
255                                 var expectedHtml = $( '<div>This has  tags</div>' ).html();
256                                 var createdHtml = $div.html();
257                                 expect( expectedHtml ).toEqual( createdHtml );
258                                 console.log( 'expected: ' + expectedHtml );
259                                 console.log( 'created: ' + createdHtml );
260                                 delete $.fn.msg;
261                         } );
265                 } );
267                 // The parser functions can throw errors, but let's not actually blow up for the user -- instead dump the error into the interface so we have
268                 // a chance at fixing this
269                 describe( "easy message interface functions with graceful failures", function() {
270                         it( "should allow a global that returns strings, with graceful failure", function() {
271                                 var gM = mw.jqueryMsg.getMessageFunction();
272                                 // passing this through jQuery and back to string, because browsers may have subtle differences, like the case of tag names.
273                                 // a surrounding <SPAN> is needed for html() to work right
274                                 var expectedHtml = $( '<span>en_fail: Parse error at position 20 in input: This should fail to {{parse</span>' ).html();
275                                 var result = gM( 'en_fail' );
276                                 expect( typeof result ).toEqual( 'string' );
277                                 expect( result ).toEqual( expectedHtml );
278                         } );
280                         it( "should allow a global that returns strings, with graceful failure on missing magic words", function() {
281                                 var gM = mw.jqueryMsg.getMessageFunction();
282                                 // passing this through jQuery and back to string, because browsers may have subtle differences, like the case of tag names.
283                                 // a surrounding <SPAN> is needed for html() to work right
284                                 var expectedHtml = $( '<span>en_fail_magic: unknown operation "sietname"</span>' ).html();
285                                 var result = gM( 'en_fail_magic' );
286                                 expect( typeof result ).toEqual( 'string' );
287                                 expect( result ).toEqual( expectedHtml );
288                         } );
291                         it( "should allow a jQuery plugin, with graceful failure", function() {
292                                 $.fn.msg = mw.jqueryMsg.getPlugin();
293                                 var $div = $( '<div>' ).append( $( '<p>' ).addClass( 'foo' ) );
294                                 $div.find( '.foo' ).msg( 'en_fail' );
295                                 // passing this through jQuery and back to string, because browsers may have subtle differences, like the case of tag names.
296                                 // a surrounding <SPAN> is needed for html() to work right
297                                 var expectedHtml = $( '<span>en_fail: Parse error at position 20 in input: This should fail to {{parse</span>' ).html();
298                                 var createdHtml = $div.find( '.foo' ).html();
299                                 expect( createdHtml ).toEqual( expectedHtml );
300                                 delete $.fn.msg;
301                         } );
303                 } );
308                 describe( "test plurals and other language-specific functions", function() {
309                         /* copying some language definitions in here -- it's hard to make this test fast and reliable 
310                            otherwise, and we don't want to have to know the mediawiki URL from this kind of test either.
311                            We also can't preload the langs for the test since they clobber the same namespace.
312                            In principle Roan said it was okay to change how languages worked so that didn't happen... maybe 
313                            someday. We'd have to the same kind of importing of the default rules for most rules, or maybe 
314                            come up with some kind of subclassing scheme for languages */
315                         var languageClasses = {
316                                 ar: {
317                                         /**
318                                          * Arabic (العربية) language functions
319                                          */
321                                         convertPlural: function( count, forms ) {
322                                                 forms = mw.language.preConvertPlural( forms, 6 );
323                                                 if ( count === 0 ) {
324                                                         return forms[0];
325                                                 }
326                                                 if ( count == 1 ) {
327                                                         return forms[1];
328                                                 }
329                                                 if ( count == 2 ) {
330                                                         return forms[2];
331                                                 }
332                                                 if ( count % 100 >= 3 && count % 100 <= 10 ) {
333                                                         return forms[3];
334                                                 }
335                                                 if ( count % 100 >= 11 && count % 100 <= 99 ) {
336                                                         return forms[4];
337                                                 }
338                                                 return forms[5];
339                                         },
341                                         digitTransformTable: {
342                                             '0': '٠', // &#x0660;
343                                             '1': '١', // &#x0661;
344                                             '2': '٢', // &#x0662;
345                                             '3': '٣', // &#x0663;
346                                             '4': '٤', // &#x0664;
347                                             '5': '٥', // &#x0665;
348                                             '6': '٦', // &#x0666;
349                                             '7': '٧', // &#x0667;
350                                             '8': '٨', // &#x0668;
351                                             '9': '٩', // &#x0669;
352                                             '.': '٫', // &#x066b; wrong table ?
353                                             ',': '٬' // &#x066c;
354                                         }
356                                 },
357                                 en: { },
358                                 fr: {
359                                         convertPlural: function( count, forms ) {
360                                                 forms = mw.language.preConvertPlural( forms, 2 );
361                                                 return ( count <= 1 ) ? forms[0] : forms[1];
362                                         }
363                                 },
364                                 jp: { },
365                                 zh: { }
366                         };
368                         /* simulate how the language classes override, or don't, the standard functions in mw.language */
369                         $.each( languageClasses, function( langCode, rules ) { 
370                                 $.each( [ 'convertPlural', 'convertNumber' ], function( i, propertyName ) { 
371                                         if ( typeof rules[ propertyName ] === 'undefined' ) {
372                                                 rules[ propertyName ] = mw.language[ propertyName ];
373                                         }
374                                 } );
375                         } );
377                         $.each( jasmineMsgSpec, function( i, test ) { 
378                                 it( "should parse " + test.name, function() { 
379                                         // using language override so we don't have to muck with global namespace
380                                         var parser = new mw.jqueryMsg.parser( { language: languageClasses[ test.lang ] } );
381                                         var parsedHtml = parser.parse( test.key, test.args ).html();
382                                         expect( parsedHtml ).toEqual( test.result );
383                                 } );
384                         } );
386                 } );
388         } );
389 } )( window.mediaWiki, jQuery );