Merge "(bug 26909) follow up r102947: fix the navigation with 'dir' and 'continue...
[lhc/web/wiklou.git] / tests / jasmine / spec / mediawiki.jqueryMsg.spec.js
1 /* spec for language & message behaviour in MediaWiki */
2
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 } );
21
22 /**
23 * Tests
24 */
25 ( function( mw, $, undefined ) {
26
27 describe( "mediaWiki.jqueryMsg", function() {
28
29 describe( "basic message functionality", function() {
30
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 } );
35
36
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 } );
41
42 } );
43
44 describe( "escaping", function() {
45
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 } );
50
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 } );
55
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 } );
60
61 } );
62
63 describe( "replacing", function() {
64
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 } );
69
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 } );
75
76 } );
77
78 describe( "linking", function() {
79
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 } );
93
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 } );
107
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 } );
130
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 } );
152
153
154 } );
155
156
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 } );
174
175
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 } );
183
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' );
190
191 } );
192 } );
193
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 } );
204
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 } );
226
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 } );
237
238
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 } );
247
248
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 } );
262
263
264
265 } );
266
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 } );
279
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 } );
289
290
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 } );
302
303 } );
304
305
306
307
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 */
320
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 },
340
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 }
355
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 };
367
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 } );
376
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 } );
385
386 } );
387
388 } );
389 } )( window.mediaWiki, jQuery );