jquery.byteLimit: Handle characters outside BMP (surrogate pairs) when trimming
[lhc/web/wiklou.git] / tests / qunit / suites / resources / jquery / jquery.byteLimit.test.js
1 ( function ( $, mw ) {
2 var simpleSample, U_20AC, poop, mbSample;
3
4 QUnit.module( 'jquery.byteLimit', QUnit.newMwEnvironment() );
5
6 // Simple sample (20 chars, 20 bytes)
7 simpleSample = '12345678901234567890';
8
9 // 3 bytes (euro-symbol)
10 U_20AC = '\u20AC';
11
12 // Outside of the BMP (pile of poo emoji)
13 poop = '\uD83D\uDCA9'; // "💩"
14
15 // Multi-byte sample (22 chars, 26 bytes)
16 mbSample = '1234567890' + U_20AC + '1234567890' + U_20AC;
17
18 // Basic sendkey-implementation
19 function addChars( $input, charstr ) {
20 var c, len;
21
22 function x( $input, i ) {
23 // Add character to the value
24 return $input.val() + charstr.charAt( i );
25 }
26
27 for ( c = 0, len = charstr.length; c < len; c += 1 ) {
28 $input
29 .val( x( $input, c ) )
30 .trigger( 'change' );
31 }
32 }
33
34 /**
35 * Test factory for $.fn.byteLimit
36 *
37 * @param {Object} options
38 * @param {string} options.description Test name
39 * @param {jQuery} options.$input jQuery object in an input element
40 * @param {string} options.sample Sequence of characters to simulate being
41 * added one by one
42 * @param {string} options.expected Expected final value of `$input`
43 */
44 function byteLimitTest( options ) {
45 var opt = $.extend( {
46 description: '',
47 $input: null,
48 sample: '',
49 expected: ''
50 }, options );
51
52 QUnit.test( opt.description, function ( assert ) {
53 opt.$input.appendTo( '#qunit-fixture' );
54
55 // Simulate pressing keys for each of the sample characters
56 addChars( opt.$input, opt.sample );
57
58 assert.equal(
59 opt.$input.val(),
60 opt.expected,
61 'New value matches the expected string'
62 );
63 } );
64 }
65
66 byteLimitTest( {
67 description: 'Plain text input',
68 $input: $( '<input>' ).attr( 'type', 'text' ),
69 sample: simpleSample,
70 expected: simpleSample
71 } );
72
73 byteLimitTest( {
74 description: 'Plain text input. Calling byteLimit with no parameters and no maxlength attribute (T38310)',
75 $input: $( '<input>' ).attr( 'type', 'text' )
76 .byteLimit(),
77 sample: simpleSample,
78 expected: simpleSample
79 } );
80
81 byteLimitTest( {
82 description: 'Limit using the maxlength attribute',
83 $input: $( '<input>' ).attr( 'type', 'text' )
84 .attr( 'maxlength', '10' )
85 .byteLimit(),
86 sample: simpleSample,
87 expected: '1234567890'
88 } );
89
90 byteLimitTest( {
91 description: 'Limit using a custom value',
92 $input: $( '<input>' ).attr( 'type', 'text' )
93 .byteLimit( 10 ),
94 sample: simpleSample,
95 expected: '1234567890'
96 } );
97
98 byteLimitTest( {
99 description: 'Limit using a custom value, overriding maxlength attribute',
100 $input: $( '<input>' ).attr( 'type', 'text' )
101 .attr( 'maxlength', '10' )
102 .byteLimit( 15 ),
103 sample: simpleSample,
104 expected: '123456789012345'
105 } );
106
107 byteLimitTest( {
108 description: 'Limit using a custom value (multibyte)',
109 $input: $( '<input>' ).attr( 'type', 'text' )
110 .byteLimit( 14 ),
111 sample: mbSample,
112 expected: '1234567890' + U_20AC + '1'
113 } );
114
115 byteLimitTest( {
116 description: 'Limit using a custom value (multibyte, outside BMP)',
117 $input: $( '<input>' ).attr( 'type', 'text' )
118 .byteLimit( 3 ),
119 sample: poop,
120 expected: ''
121 } );
122
123 byteLimitTest( {
124 description: 'Limit using a custom value (multibyte) overlapping a byte',
125 $input: $( '<input>' ).attr( 'type', 'text' )
126 .byteLimit( 12 ),
127 sample: mbSample,
128 expected: '123456789012'
129 } );
130
131 byteLimitTest( {
132 description: 'Pass the limit and a callback as input filter',
133 $input: $( '<input>' ).attr( 'type', 'text' )
134 .byteLimit( 6, function ( val ) {
135 var title = mw.Title.newFromText( String( val ) );
136 // Return without namespace prefix
137 return title ? title.getMain() : '';
138 } ),
139 sample: 'User:Sample',
140 expected: 'User:Sample'
141 } );
142
143 byteLimitTest( {
144 description: 'Limit using the maxlength attribute and pass a callback as input filter',
145 $input: $( '<input>' ).attr( 'type', 'text' )
146 .attr( 'maxlength', '6' )
147 .byteLimit( function ( val ) {
148 var title = mw.Title.newFromText( String( val ) );
149 // Return without namespace prefix
150 return title ? title.getMain() : '';
151 } ),
152 sample: 'User:Sample',
153 expected: 'User:Sample'
154 } );
155
156 byteLimitTest( {
157 description: 'Pass the limit and a callback as input filter',
158 $input: $( '<input>' ).attr( 'type', 'text' )
159 .byteLimit( 6, function ( val ) {
160 var title = mw.Title.newFromText( String( val ) );
161 // Return without namespace prefix
162 return title ? title.getMain() : '';
163 } ),
164 sample: 'User:Example',
165 // The callback alters the value to be used to calculeate
166 // the length. The altered value is "Exampl" which has
167 // a length of 6, the "e" would exceed the limit.
168 expected: 'User:Exampl'
169 } );
170
171 byteLimitTest( {
172 description: 'Input filter that increases the length',
173 $input: $( '<input>' ).attr( 'type', 'text' )
174 .byteLimit( 10, function ( text ) {
175 return 'prefix' + text;
176 } ),
177 sample: simpleSample,
178 // Prefix adds 6 characters, limit is reached after 4
179 expected: '1234'
180 } );
181
182 // Regression tests for T43450
183 byteLimitTest( {
184 description: 'Input filter of which the base exceeds the limit',
185 $input: $( '<input>' ).attr( 'type', 'text' )
186 .byteLimit( 3, function ( text ) {
187 return 'prefix' + text;
188 } ),
189 sample: simpleSample,
190 hasLimit: true,
191 limit: 6, // 'prefix' length
192 expected: ''
193 } );
194
195 QUnit.test( 'Confirm properties and attributes set', function ( assert ) {
196 var $el;
197
198 $el = $( '<input>' ).attr( 'type', 'text' )
199 .attr( 'maxlength', '7' )
200 .appendTo( '#qunit-fixture' )
201 .byteLimit();
202
203 assert.strictEqual( $el.attr( 'maxlength' ), '7', 'maxlength attribute unchanged for simple limit' );
204
205 $el = $( '<input>' ).attr( 'type', 'text' )
206 .attr( 'maxlength', '7' )
207 .appendTo( '#qunit-fixture' )
208 .byteLimit( 12 );
209
210 assert.strictEqual( $el.attr( 'maxlength' ), '12', 'maxlength attribute updated for custom limit' );
211
212 $el = $( '<input>' ).attr( 'type', 'text' )
213 .attr( 'maxlength', '7' )
214 .appendTo( '#qunit-fixture' )
215 .byteLimit( 12, function ( val ) {
216 return val;
217 } );
218
219 assert.strictEqual( $el.attr( 'maxlength' ), undefined, 'maxlength attribute removed for limit with callback' );
220
221 $( '<input>' ).attr( 'type', 'text' )
222 .addClass( 'mw-test-byteLimit-foo' )
223 .attr( 'maxlength', '7' )
224 .appendTo( '#qunit-fixture' );
225
226 $( '<input>' ).attr( 'type', 'text' )
227 .addClass( 'mw-test-byteLimit-foo' )
228 .attr( 'maxlength', '12' )
229 .appendTo( '#qunit-fixture' );
230
231 $el = $( '.mw-test-byteLimit-foo' );
232
233 assert.strictEqual( $el.length, 2, 'Verify that there are no other elements clashing with this test suite' );
234
235 $el.byteLimit();
236 } );
237
238 QUnit.test( 'Trim from insertion when limit exceeded', function ( assert ) {
239 var $el;
240
241 // Use a new <input> because the bug only occurs on the first time
242 // the limit it reached (T42850)
243 $el = $( '<input>' ).attr( 'type', 'text' )
244 .appendTo( '#qunit-fixture' )
245 .byteLimit( 3 )
246 .val( 'abc' ).trigger( 'change' )
247 .val( 'zabc' ).trigger( 'change' );
248
249 assert.strictEqual( $el.val(), 'abc', 'Trim from the insertion point (at 0), not the end' );
250
251 $el = $( '<input>' ).attr( 'type', 'text' )
252 .appendTo( '#qunit-fixture' )
253 .byteLimit( 3 )
254 .val( 'abc' ).trigger( 'change' )
255 .val( 'azbc' ).trigger( 'change' );
256
257 assert.strictEqual( $el.val(), 'abc', 'Trim from the insertion point (at 1), not the end' );
258 } );
259
260 QUnit.test( 'Do not cut up false matching substrings in emoji insertions', function ( assert ) {
261 var $el,
262 oldVal = '\uD83D\uDCA9\uD83D\uDCA9', // "💩💩"
263 newVal = '\uD83D\uDCA9\uD83D\uDCB9\uD83E\uDCA9\uD83D\uDCA9', // "💩💹🢩💩"
264 expected = '\uD83D\uDCA9\uD83D\uDCB9\uD83D\uDCA9'; // "💩💹💩"
265
266 // Possible bad results:
267 // * With no surrogate support:
268 // '\uD83D\uDCA9\uD83D\uDCB9\uD83E\uDCA9' "💩💹🢩"
269 // * With correct trimming but bad detection of inserted text:
270 // '\uD83D\uDCA9\uD83D\uDCB9\uDCA9' "💩💹�"
271
272 $el = $( '<input>' ).attr( 'type', 'text' )
273 .appendTo( '#qunit-fixture' )
274 .byteLimit( 12 )
275 .val( oldVal ).trigger( 'change' )
276 .val( newVal ).trigger( 'change' );
277
278 assert.strictEqual( $el.val(), expected, 'Pasted emoji correctly trimmed at the end' );
279 } );
280
281 byteLimitTest( {
282 description: 'Unpaired surrogates do not crash',
283 $input: $( '<input>' ).attr( 'type', 'text' ).byteLimit( 4 ),
284 sample: '\uD800\uD800\uDFFF',
285 expected: '\uD800'
286 } );
287
288 }( jQuery, mediaWiki ) );