Merge "Revised styling of sister-search sidebar."
[lhc/web/wiklou.git] / tests / qunit / suites / resources / mediawiki.rcfilters / dm.FiltersViewModel.test.js
1 /* eslint-disable camelcase */
2 ( function ( mw, $ ) {
3 QUnit.module( 'mediawiki.rcfilters - FiltersViewModel', QUnit.newMwEnvironment( {
4 messages: {
5 'group1filter1-label': 'Group 1: Filter 1',
6 'group1filter1-desc': 'Description of Filter 1 in Group 1',
7 'group1filter2-label': 'Group 1: Filter 2',
8 'group1filter2-desc': 'Description of Filter 2 in Group 1',
9 'group2filter1-label': 'Group 2: Filter 1',
10 'group2filter1-desc': 'Description of Filter 1 in Group 2',
11 'group2filter2-label': 'xGroup 2: Filter 2',
12 'group2filter2-desc': 'Description of Filter 2 in Group 2'
13 }
14 } ) );
15
16 QUnit.test( 'Setting up filters', function ( assert ) {
17 var definition = [ {
18 name: 'group1',
19 title: 'Group 1',
20 type: 'send_unselected_if_any',
21 filters: [
22 {
23 name: 'filter1',
24 label: 'Group 1: Filter 1',
25 description: 'Description of Filter 1 in Group 1'
26 },
27 {
28 name: 'filter2',
29 label: 'Group 1: Filter 2',
30 description: 'Description of Filter 2 in Group 1'
31 }
32 ]
33 }, {
34 name: 'group2',
35 title: 'Group 2',
36 type: 'send_unselected_if_any',
37 filters: [
38 {
39 name: 'filter1',
40 label: 'Group 2: Filter 1',
41 description: 'Description of Filter 1 in Group 2'
42 },
43 {
44 name: 'filter2',
45 label: 'Group 2: Filter 2',
46 description: 'Description of Filter 2 in Group 2'
47 }
48 ]
49 }, {
50 name: 'group3',
51 title: 'Group 3',
52 type: 'string_options',
53 filters: [
54 {
55 name: 'filter1',
56 label: 'Group 3: Filter 1',
57 description: 'Description of Filter 1 in Group 3'
58 },
59 {
60 name: 'filter2',
61 label: 'Group 3: Filter 2',
62 description: 'Description of Filter 2 in Group 3'
63 }
64 ]
65 } ],
66 model = new mw.rcfilters.dm.FiltersViewModel();
67
68 model.initializeFilters( definition );
69
70 assert.ok(
71 model.getItemByName( 'group1__filter1' ) instanceof mw.rcfilters.dm.FilterItem &&
72 model.getItemByName( 'group1__filter2' ) instanceof mw.rcfilters.dm.FilterItem &&
73 model.getItemByName( 'group2__filter1' ) instanceof mw.rcfilters.dm.FilterItem &&
74 model.getItemByName( 'group2__filter2' ) instanceof mw.rcfilters.dm.FilterItem &&
75 model.getItemByName( 'group3__filter1' ) instanceof mw.rcfilters.dm.FilterItem &&
76 model.getItemByName( 'group3__filter2' ) instanceof mw.rcfilters.dm.FilterItem,
77 'Filters instantiated and stored correctly'
78 );
79
80 assert.deepEqual(
81 model.getSelectedState(),
82 {
83 group1__filter1: false,
84 group1__filter2: false,
85 group2__filter1: false,
86 group2__filter2: false,
87 group3__filter1: false,
88 group3__filter2: false
89 },
90 'Initial state of filters'
91 );
92
93 model.toggleFiltersSelected( {
94 group1__filter1: true,
95 group2__filter2: true,
96 group3__filter1: true
97 } );
98 assert.deepEqual(
99 model.getSelectedState(),
100 {
101 group1__filter1: true,
102 group1__filter2: false,
103 group2__filter1: false,
104 group2__filter2: true,
105 group3__filter1: true,
106 group3__filter2: false
107 },
108 'Updating filter states correctly'
109 );
110 } );
111
112 QUnit.test( 'Finding matching filters', function ( assert ) {
113 var matches,
114 definition = [ {
115 name: 'group1',
116 title: 'Group 1 title',
117 type: 'send_unselected_if_any',
118 filters: [
119 {
120 name: 'filter1',
121 label: 'group1filter1-label',
122 description: 'group1filter1-desc'
123 },
124 {
125 name: 'filter2',
126 label: 'group1filter2-label',
127 description: 'group1filter2-desc'
128 }
129 ]
130 }, {
131 name: 'group2',
132 title: 'Group 2 title',
133 type: 'send_unselected_if_any',
134 filters: [
135 {
136 name: 'filter1',
137 label: 'group2filter1-label',
138 description: 'group2filter1-desc'
139 },
140 {
141 name: 'filter2',
142 label: 'group2filter2-label',
143 description: 'group2filter2-desc'
144 }
145 ]
146 } ],
147 testCases = [
148 {
149 query: 'group',
150 expectedMatches: {
151 group1: [ 'group1__filter1', 'group1__filter2' ],
152 group2: [ 'group2__filter1' ]
153 },
154 reason: 'Finds filters starting with the query string'
155 },
156 {
157 query: 'filter 2 in group',
158 expectedMatches: {
159 group1: [ 'group1__filter2' ],
160 group2: [ 'group2__filter2' ]
161 },
162 reason: 'Finds filters containing the query string in their description'
163 },
164 {
165 query: 'title',
166 expectedMatches: {
167 group1: [ 'group1__filter1', 'group1__filter2' ],
168 group2: [ 'group2__filter1', 'group2__filter2' ]
169 },
170 reason: 'Finds filters containing the query string in their group title'
171 }
172 ],
173 model = new mw.rcfilters.dm.FiltersViewModel(),
174 extractNames = function ( matches ) {
175 var result = {};
176 Object.keys( matches ).forEach( function ( groupName ) {
177 result[ groupName ] = matches[ groupName ].map( function ( item ) {
178 return item.getName();
179 } );
180 } );
181 return result;
182 };
183
184 model.initializeFilters( definition );
185
186 testCases.forEach( function ( testCase ) {
187 matches = model.findMatches( testCase.query );
188 assert.deepEqual(
189 extractNames( matches ),
190 testCase.expectedMatches,
191 testCase.reason
192 );
193 } );
194
195 matches = model.findMatches( 'foo' );
196 assert.ok(
197 $.isEmptyObject( matches ),
198 'findMatches returns an empty object when no results found'
199 );
200 } );
201
202 QUnit.test( 'getParametersFromFilters', function ( assert ) {
203 var definition = [ {
204 name: 'group1',
205 title: 'Group 1',
206 type: 'send_unselected_if_any',
207 filters: [
208 {
209 name: 'hidefilter1',
210 label: 'Group 1: Filter 1',
211 description: 'Description of Filter 1 in Group 1'
212 },
213 {
214 name: 'hidefilter2',
215 label: 'Group 1: Filter 2',
216 description: 'Description of Filter 2 in Group 1'
217 },
218 {
219 name: 'hidefilter3',
220 label: 'Group 1: Filter 3',
221 description: 'Description of Filter 3 in Group 1'
222 }
223 ]
224 }, {
225 name: 'group2',
226 title: 'Group 2',
227 type: 'send_unselected_if_any',
228 filters: [
229 {
230 name: 'hidefilter4',
231 label: 'Group 2: Filter 1',
232 description: 'Description of Filter 1 in Group 2'
233 },
234 {
235 name: 'hidefilter5',
236 label: 'Group 2: Filter 2',
237 description: 'Description of Filter 2 in Group 2'
238 },
239 {
240 name: 'hidefilter6',
241 label: 'Group 2: Filter 3',
242 description: 'Description of Filter 3 in Group 2'
243 }
244 ]
245 }, {
246 name: 'group3',
247 title: 'Group 3',
248 type: 'string_options',
249 separator: ',',
250 filters: [
251 {
252 name: 'filter7',
253 label: 'Group 3: Filter 1',
254 description: 'Description of Filter 1 in Group 3'
255 },
256 {
257 name: 'filter8',
258 label: 'Group 3: Filter 2',
259 description: 'Description of Filter 2 in Group 3'
260 },
261 {
262 name: 'filter9',
263 label: 'Group 3: Filter 3',
264 description: 'Description of Filter 3 in Group 3'
265 }
266 ]
267 } ],
268 model = new mw.rcfilters.dm.FiltersViewModel();
269
270 model.initializeFilters( definition );
271
272 // Starting with all filters unselected
273 assert.deepEqual(
274 model.getParametersFromFilters(),
275 {
276 hidefilter1: 0,
277 hidefilter2: 0,
278 hidefilter3: 0,
279 hidefilter4: 0,
280 hidefilter5: 0,
281 hidefilter6: 0,
282 group3: ''
283 },
284 'Unselected filters return all parameters falsey or \'\'.'
285 );
286
287 // Select 1 filter
288 model.toggleFiltersSelected( {
289 group1__hidefilter1: true,
290 group1__hidefilter2: false,
291 group1__hidefilter3: false,
292 group2__hidefilter4: false,
293 group2__hidefilter5: false,
294 group2__hidefilter6: false
295 } );
296 // Only one filter in one group
297 assert.deepEqual(
298 model.getParametersFromFilters(),
299 {
300 // Group 1 (one selected, the others are true)
301 hidefilter1: 0,
302 hidefilter2: 1,
303 hidefilter3: 1,
304 // Group 2 (nothing is selected, all false)
305 hidefilter4: 0,
306 hidefilter5: 0,
307 hidefilter6: 0,
308 group3: ''
309 },
310 'One filters in one "send_unselected_if_any" group returns the other parameters truthy.'
311 );
312
313 // Select 2 filters
314 model.toggleFiltersSelected( {
315 group1__hidefilter1: true,
316 group1__hidefilter2: true,
317 group1__hidefilter3: false,
318 group2__hidefilter4: false,
319 group2__hidefilter5: false,
320 group2__hidefilter6: false
321 } );
322 // Two selected filters in one group
323 assert.deepEqual(
324 model.getParametersFromFilters(),
325 {
326 // Group 1 (two selected, the others are true)
327 hidefilter1: 0,
328 hidefilter2: 0,
329 hidefilter3: 1,
330 // Group 2 (nothing is selected, all false)
331 hidefilter4: 0,
332 hidefilter5: 0,
333 hidefilter6: 0,
334 group3: ''
335 },
336 'Two filters in one "send_unselected_if_any" group returns the other parameters truthy.'
337 );
338
339 // Select 3 filters
340 model.toggleFiltersSelected( {
341 group1__hidefilter1: true,
342 group1__hidefilter2: true,
343 group1__hidefilter3: true,
344 group2__hidefilter4: false,
345 group2__hidefilter5: false,
346 group2__hidefilter6: false
347 } );
348 // All filters of the group are selected == this is the same as not selecting any
349 assert.deepEqual(
350 model.getParametersFromFilters(),
351 {
352 // Group 1 (all selected, all false)
353 hidefilter1: 0,
354 hidefilter2: 0,
355 hidefilter3: 0,
356 // Group 2 (nothing is selected, all false)
357 hidefilter4: 0,
358 hidefilter5: 0,
359 hidefilter6: 0,
360 group3: ''
361 },
362 'All filters selected in one "send_unselected_if_any" group returns all parameters falsy.'
363 );
364
365 // Select 1 filter from string_options
366 model.toggleFiltersSelected( {
367 group3__filter7: true,
368 group3__filter8: false,
369 group3__filter9: false
370 } );
371 // All filters of the group are selected == this is the same as not selecting any
372 assert.deepEqual(
373 model.getParametersFromFilters(),
374 {
375 // Group 1 (all selected, all)
376 hidefilter1: 0,
377 hidefilter2: 0,
378 hidefilter3: 0,
379 // Group 2 (nothing is selected, all false)
380 hidefilter4: 0,
381 hidefilter5: 0,
382 hidefilter6: 0,
383 group3: 'filter7'
384 },
385 'One filter selected in "string_option" group returns that filter in the value.'
386 );
387
388 // Select 2 filters from string_options
389 model.toggleFiltersSelected( {
390 group3__filter7: true,
391 group3__filter8: true,
392 group3__filter9: false
393 } );
394 // All filters of the group are selected == this is the same as not selecting any
395 assert.deepEqual(
396 model.getParametersFromFilters(),
397 {
398 // Group 1 (all selected, all)
399 hidefilter1: 0,
400 hidefilter2: 0,
401 hidefilter3: 0,
402 // Group 2 (nothing is selected, all false)
403 hidefilter4: 0,
404 hidefilter5: 0,
405 hidefilter6: 0,
406 group3: 'filter7,filter8'
407 },
408 'Two filters selected in "string_option" group returns those filters in the value.'
409 );
410
411 // Select 3 filters from string_options
412 model.toggleFiltersSelected( {
413 group3__filter7: true,
414 group3__filter8: true,
415 group3__filter9: true
416 } );
417 // All filters of the group are selected == this is the same as not selecting any
418 assert.deepEqual(
419 model.getParametersFromFilters(),
420 {
421 // Group 1 (all selected, all)
422 hidefilter1: 0,
423 hidefilter2: 0,
424 hidefilter3: 0,
425 // Group 2 (nothing is selected, all false)
426 hidefilter4: 0,
427 hidefilter5: 0,
428 hidefilter6: 0,
429 group3: 'all'
430 },
431 'All filters selected in "string_option" group returns \'all\'.'
432 );
433
434 } );
435
436 QUnit.test( 'getParametersFromFilters (custom object)', function ( assert ) {
437 var originalState,
438 model = new mw.rcfilters.dm.FiltersViewModel(),
439 definition = [ {
440 name: 'group1',
441 title: 'Group 1',
442 type: 'send_unselected_if_any',
443 filters: [
444 { name: 'hidefilter1', label: 'Hide filter 1', description: '' },
445 { name: 'hidefilter2', label: 'Hide filter 2', description: '' },
446 { name: 'hidefilter3', label: 'Hide filter 3', description: '' }
447 ]
448 }, {
449 name: 'group2',
450 title: 'Group 2',
451 type: 'send_unselected_if_any',
452 filters: [
453 { name: 'hidefilter4', label: 'Hide filter 4', description: '' },
454 { name: 'hidefilter5', label: 'Hide filter 5', description: '' },
455 { name: 'hidefilter6', label: 'Hide filter 6', description: '' }
456 ]
457 }, {
458 name: 'group3',
459 title: 'Group 3',
460 type: 'string_options',
461 separator: ',',
462 filters: [
463 { name: 'filter7', label: 'Hide filter 7', description: '' },
464 { name: 'filter8', label: 'Hide filter 8', description: '' },
465 { name: 'filter9', label: 'Hide filter 9', description: '' }
466 ]
467 } ],
468 cases = [
469 {
470 // This is mocking the cases above, both
471 // - 'Two filters in one "send_unselected_if_any" group returns the other parameters truthy.'
472 // - 'Two filters selected in "string_option" group returns those filters in the value.'
473 input: {
474 group1__hidefilter1: true,
475 group1__hidefilter2: true,
476 group1__hidefilter3: false,
477 group2__hidefilter4: false,
478 group2__hidefilter5: false,
479 group2__hidefilter6: false,
480 group3__filter7: true,
481 group3__filter8: true,
482 group3__filter9: false
483 },
484 expected: {
485 // Group 1 (two selected, the others are true)
486 hidefilter1: 0,
487 hidefilter2: 0,
488 hidefilter3: 1,
489 // Group 2 (nothing is selected, all false)
490 hidefilter4: 0,
491 hidefilter5: 0,
492 hidefilter6: 0,
493 group3: 'filter7,filter8'
494 },
495 msg: 'Given an explicit (complete) filter state object, the result is the same as if the object given represented the model state.'
496 },
497 {
498 // This is mocking case above
499 // - 'One filters in one "send_unselected_if_any" group returns the other parameters truthy.'
500 input: {
501 group1__hidefilter1: 1
502 },
503 expected: {
504 // Group 1 (one selected, the others are true)
505 hidefilter1: 0,
506 hidefilter2: 1,
507 hidefilter3: 1,
508 // Group 2 (nothing is selected, all false)
509 hidefilter4: 0,
510 hidefilter5: 0,
511 hidefilter6: 0,
512 group3: ''
513 },
514 msg: 'Given an explicit (incomplete) filter state object, the result is the same as if the object give represented the model state.'
515 }
516 ];
517
518 model.initializeFilters( definition );
519 // Store original state
520 originalState = model.getSelectedState();
521
522 // Test each case
523 cases.forEach( function ( test ) {
524 assert.deepEqual(
525 model.getParametersFromFilters( test.input ),
526 test.expected,
527 test.msg
528 );
529 } );
530
531 // After doing the above tests, make sure the actual state
532 // of the filter stayed the same
533 assert.deepEqual(
534 model.getSelectedState(),
535 originalState,
536 'Running the method with external definition to parse does not actually change the state of the model'
537 );
538 } );
539
540 QUnit.test( 'getFiltersFromParameters', function ( assert ) {
541 var definition = [ {
542 name: 'group1',
543 title: 'Group 1',
544 type: 'send_unselected_if_any',
545 filters: [
546 {
547 name: 'hidefilter1',
548 label: 'Show filter 1',
549 description: 'Description of Filter 1 in Group 1',
550 default: true
551 },
552 {
553 name: 'hidefilter2',
554 label: 'Show filter 2',
555 description: 'Description of Filter 2 in Group 1'
556 },
557 {
558 name: 'hidefilter3',
559 label: 'Show filter 3',
560 description: 'Description of Filter 3 in Group 1',
561 default: true
562 }
563 ]
564 }, {
565 name: 'group2',
566 title: 'Group 2',
567 type: 'send_unselected_if_any',
568 filters: [
569 {
570 name: 'hidefilter4',
571 label: 'Show filter 4',
572 description: 'Description of Filter 1 in Group 2'
573 },
574 {
575 name: 'hidefilter5',
576 label: 'Show filter 5',
577 description: 'Description of Filter 2 in Group 2',
578 default: true
579 },
580 {
581 name: 'hidefilter6',
582 label: 'Show filter 6',
583 description: 'Description of Filter 3 in Group 2'
584 }
585 ]
586 }, {
587
588 name: 'group3',
589 title: 'Group 3',
590 type: 'string_options',
591 separator: ',',
592 default: 'filter8',
593 filters: [
594 {
595 name: 'filter7',
596 label: 'Group 3: Filter 1',
597 description: 'Description of Filter 1 in Group 3'
598 },
599 {
600 name: 'filter8',
601 label: 'Group 3: Filter 2',
602 description: 'Description of Filter 2 in Group 3'
603 },
604 {
605 name: 'filter9',
606 label: 'Group 3: Filter 3',
607 description: 'Description of Filter 3 in Group 3'
608 }
609 ]
610 } ],
611 defaultFilterRepresentation = {
612 // Group 1 and 2, "send_unselected_if_any", the values of the filters are "flipped" from the values of the parameters
613 group1__hidefilter1: false,
614 group1__hidefilter2: true,
615 group1__hidefilter3: false,
616 group2__hidefilter4: true,
617 group2__hidefilter5: false,
618 group2__hidefilter6: true,
619 // Group 3, "string_options", default values correspond to parameters and filters
620 group3__filter7: false,
621 group3__filter8: true,
622 group3__filter9: false
623 },
624 model = new mw.rcfilters.dm.FiltersViewModel();
625
626 model.initializeFilters( definition );
627
628 // Empty query = only default values
629 assert.deepEqual(
630 model.getFiltersFromParameters( {} ),
631 defaultFilterRepresentation,
632 'Empty parameter query results in filters in initial default state'
633 );
634
635 assert.deepEqual(
636 model.getFiltersFromParameters( {
637 hidefilter2: '1'
638 } ),
639 $.extend( {}, defaultFilterRepresentation, {
640 group1__hidefilter1: false, // The text is "show filter 1"
641 group1__hidefilter2: false, // The text is "show filter 2"
642 group1__hidefilter3: false // The text is "show filter 3"
643 } ),
644 'One truthy parameter in a group whose other parameters are true by default makes the rest of the filters in the group false (unchecked)'
645 );
646
647 assert.deepEqual(
648 model.getFiltersFromParameters( {
649 hidefilter1: '1',
650 hidefilter2: '1',
651 hidefilter3: '1'
652 } ),
653 $.extend( {}, defaultFilterRepresentation, {
654 group1__hidefilter1: false, // The text is "show filter 1"
655 group1__hidefilter2: false, // The text is "show filter 2"
656 group1__hidefilter3: false // The text is "show filter 3"
657 } ),
658 'All paremeters in the same \'send_unselected_if_any\' group false is equivalent to none are truthy (checked) in the interface'
659 );
660
661 // The ones above don't update the model, so we have a clean state.
662 // getFiltersFromParameters is stateless; any change is unaffected by the current state
663 // This test is demonstrating wrong usage of the method;
664 // We should be aware that getFiltersFromParameters is stateless,
665 // so each call gives us a filter state that only reflects the query given.
666 // This means that the two calls to toggleFiltersSelected() below collide.
667 // The result of the first is overridden by the result of the second,
668 // since both get a full state object from getFiltersFromParameters that **only** relates
669 // to the input it receives.
670 model.toggleFiltersSelected(
671 model.getFiltersFromParameters( {
672 hidefilter1: '1'
673 } )
674 );
675
676 model.toggleFiltersSelected(
677 model.getFiltersFromParameters( {
678 hidefilter6: '1'
679 } )
680 );
681
682 // The result here is ignoring the first toggleFiltersSelected call
683 // We should receive default values + hidefilter6 as false
684 assert.deepEqual(
685 model.getSelectedState(),
686 $.extend( {}, defaultFilterRepresentation, {
687 group2__hidefilter5: false,
688 group2__hidefilter6: false
689 } ),
690 'getFiltersFromParameters does not care about previous or existing state.'
691 );
692
693 // Reset
694 model = new mw.rcfilters.dm.FiltersViewModel();
695 model.initializeFilters( definition );
696
697 model.toggleFiltersSelected(
698 model.getFiltersFromParameters( {
699 hidefilter1: '0'
700 } )
701 );
702 model.toggleFiltersSelected(
703 model.getFiltersFromParameters( {
704 hidefilter1: '1'
705 } )
706 );
707
708 // Simulates minor edits being hidden in preferences, then unhidden via URL
709 // override.
710 assert.deepEqual(
711 model.getSelectedState(),
712 defaultFilterRepresentation,
713 'After checking and then unchecking a \'send_unselected_if_any\' filter (without touching other filters in that group), results are default'
714 );
715
716 model.toggleFiltersSelected(
717 model.getFiltersFromParameters( {
718 group3: 'filter7'
719 } )
720 );
721 assert.deepEqual(
722 model.getSelectedState(),
723 $.extend( {}, defaultFilterRepresentation, {
724 group3__filter7: true,
725 group3__filter8: false,
726 group3__filter9: false
727 } ),
728 'A \'string_options\' parameter containing 1 value, results in the corresponding filter as checked'
729 );
730
731 model.toggleFiltersSelected(
732 model.getFiltersFromParameters( {
733 group3: 'filter7,filter8'
734 } )
735 );
736 assert.deepEqual(
737 model.getSelectedState(),
738 $.extend( {}, defaultFilterRepresentation, {
739 group3__filter7: true,
740 group3__filter8: true,
741 group3__filter9: false
742 } ),
743 'A \'string_options\' parameter containing 2 values, results in both corresponding filters as checked'
744 );
745
746 model.toggleFiltersSelected(
747 model.getFiltersFromParameters( {
748 group3: 'filter7,filter8,filter9'
749 } )
750 );
751 assert.deepEqual(
752 model.getSelectedState(),
753 $.extend( {}, defaultFilterRepresentation, {
754 group3__filter7: true,
755 group3__filter8: true,
756 group3__filter9: true
757 } ),
758 'A \'string_options\' parameter containing all values, results in all filters of the group as checked.'
759 );
760
761 model.toggleFiltersSelected(
762 model.getFiltersFromParameters( {
763 group3: 'filter7,all,filter9'
764 } )
765 );
766 assert.deepEqual(
767 model.getSelectedState(),
768 $.extend( {}, defaultFilterRepresentation, {
769 group3__filter7: true,
770 group3__filter8: true,
771 group3__filter9: true
772 } ),
773 'A \'string_options\' parameter containing the value \'all\', results in all filters of the group as checked.'
774 );
775
776 model.toggleFiltersSelected(
777 model.getFiltersFromParameters( {
778 group3: 'filter7,foo,filter9'
779 } )
780 );
781 assert.deepEqual(
782 model.getSelectedState(),
783 $.extend( {}, defaultFilterRepresentation, {
784 group3__filter7: true,
785 group3__filter8: false,
786 group3__filter9: true
787 } ),
788 'A \'string_options\' parameter containing an invalid value, results in the invalid value ignored and the valid corresponding filters checked.'
789 );
790 } );
791
792 QUnit.test( 'sanitizeStringOptionGroup', function ( assert ) {
793 var definition = [ {
794 name: 'group1',
795 title: 'Group 1',
796 type: 'string_options',
797 filters: [
798 {
799 name: 'filter1',
800 label: 'Show filter 1',
801 description: 'Description of Filter 1 in Group 1'
802 },
803 {
804 name: 'filter2',
805 label: 'Show filter 2',
806 description: 'Description of Filter 2 in Group 1'
807 },
808 {
809 name: 'filter3',
810 label: 'Show filter 3',
811 description: 'Description of Filter 3 in Group 1'
812 }
813 ]
814 } ],
815 model = new mw.rcfilters.dm.FiltersViewModel();
816
817 model.initializeFilters( definition );
818
819 assert.deepEqual(
820 model.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'filter1', 'filter2' ] ),
821 [ 'filter1', 'filter2' ],
822 'Remove duplicate values'
823 );
824
825 assert.deepEqual(
826 model.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'foo', 'filter2' ] ),
827 [ 'filter1', 'filter2' ],
828 'Remove invalid values'
829 );
830
831 assert.deepEqual(
832 model.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'all', 'filter2' ] ),
833 [ 'all' ],
834 'If any value is "all", the only value is "all".'
835 );
836 } );
837
838 QUnit.test( 'Filter interaction: subsets', function ( assert ) {
839 var definition = [ {
840 name: 'group1',
841 title: 'Group 1',
842 type: 'string_options',
843 filters: [
844 {
845 name: 'filter1',
846 label: 'Show filter 1',
847 description: 'Description of Filter 1 in Group 1',
848 subset: [
849 {
850 group: 'group1',
851 filter: 'filter2'
852 },
853 {
854 group: 'group1',
855 filter: 'filter3'
856 }
857 ]
858 },
859 {
860 name: 'filter2',
861 label: 'Show filter 2',
862 description: 'Description of Filter 2 in Group 1',
863 subset: [
864 {
865 group: 'group1',
866 filter: 'filter3'
867 }
868 ]
869 },
870 {
871 name: 'filter3',
872 label: 'Show filter 3',
873 description: 'Description of Filter 3 in Group 1'
874 }
875 ]
876 } ],
877 baseFullState = {
878 group1__filter1: { selected: false, conflicted: false, included: false },
879 group1__filter2: { selected: false, conflicted: false, included: false },
880 group1__filter3: { selected: false, conflicted: false, included: false }
881 },
882 model = new mw.rcfilters.dm.FiltersViewModel();
883
884 model.initializeFilters( definition );
885 // Select a filter that has subset with another filter
886 model.toggleFiltersSelected( {
887 group1__filter1: true
888 } );
889
890 model.reassessFilterInteractions( model.getItemByName( 'group1__filter1' ) );
891 assert.deepEqual(
892 model.getFullState(),
893 $.extend( true, {}, baseFullState, {
894 group1__filter1: { selected: true },
895 group1__filter2: { included: true },
896 group1__filter3: { included: true }
897 } ),
898 'Filters with subsets are represented in the model.'
899 );
900
901 // Select another filter that has a subset with the same previous filter
902 model.toggleFiltersSelected( {
903 group1__filter2: true
904 } );
905 model.reassessFilterInteractions( model.getItemByName( 'filter2' ) );
906 assert.deepEqual(
907 model.getFullState(),
908 $.extend( true, {}, baseFullState, {
909 group1__filter1: { selected: true },
910 group1__filter2: { selected: true, included: true },
911 group1__filter3: { included: true }
912 } ),
913 'Filters that have multiple subsets are represented.'
914 );
915
916 // Remove one filter (but leave the other) that affects filter3
917 model.toggleFiltersSelected( {
918 group1__filter1: false
919 } );
920 model.reassessFilterInteractions( model.getItemByName( 'group1__filter1' ) );
921 assert.deepEqual(
922 model.getFullState(),
923 $.extend( true, {}, baseFullState, {
924 group1__filter2: { selected: true, included: false },
925 group1__filter3: { included: true }
926 } ),
927 'Removing a filter only un-includes its subset if there is no other filter affecting.'
928 );
929
930 model.toggleFiltersSelected( {
931 group1__filter2: false
932 } );
933 model.reassessFilterInteractions( model.getItemByName( 'group1__filter2' ) );
934 assert.deepEqual(
935 model.getFullState(),
936 baseFullState,
937 'Removing all supersets also un-includes the subsets.'
938 );
939 } );
940
941 QUnit.test( 'Filter interaction: full coverage', function ( assert ) {
942 var definition = [ {
943 name: 'group1',
944 title: 'Group 1',
945 type: 'string_options',
946 fullCoverage: false,
947 filters: [
948 { name: 'filter1', label: '1', description: '1' },
949 { name: 'filter2', label: '2', description: '2' },
950 { name: 'filter3', label: '3', description: '3' }
951 ]
952 }, {
953 name: 'group2',
954 title: 'Group 2',
955 type: 'send_unselected_if_any',
956 fullCoverage: true,
957 filters: [
958 { name: 'filter4', label: '4', description: '4' },
959 { name: 'filter5', label: '5', description: '5' },
960 { name: 'filter6', label: '6', description: '6' }
961 ]
962 } ],
963 model = new mw.rcfilters.dm.FiltersViewModel(),
964 isCapsuleItemMuted = function ( filterName ) {
965 var itemModel = model.getItemByName( filterName ),
966 groupModel = itemModel.getGroupModel();
967
968 // This is the logic inside the capsule widget
969 return (
970 // The capsule item widget only appears if the item is selected
971 itemModel.isSelected() &&
972 // Muted state is only valid if group is full coverage and all items are selected
973 groupModel.isFullCoverage() && groupModel.areAllSelected()
974 );
975 },
976 getCurrentItemsMutedState = function () {
977 return {
978 group1__filter1: isCapsuleItemMuted( 'group1__filter1' ),
979 group1__filter2: isCapsuleItemMuted( 'group1__filter2' ),
980 group1__filter3: isCapsuleItemMuted( 'group1__filter3' ),
981 group2__filter4: isCapsuleItemMuted( 'group2__filter4' ),
982 group2__filter5: isCapsuleItemMuted( 'group2__filter5' ),
983 group2__filter6: isCapsuleItemMuted( 'group2__filter6' )
984 };
985 },
986 baseMuteState = {
987 group1__filter1: false,
988 group1__filter2: false,
989 group1__filter3: false,
990 group2__filter4: false,
991 group2__filter5: false,
992 group2__filter6: false
993 };
994
995 model.initializeFilters( definition );
996
997 // Starting state, no selection, all items are non-muted
998 assert.deepEqual(
999 getCurrentItemsMutedState(),
1000 baseMuteState,
1001 'No selection - all items are non-muted'
1002 );
1003
1004 // Select most (but not all) items in each group
1005 model.toggleFiltersSelected( {
1006 group1__filter1: true,
1007 group1__filter2: true,
1008 group2__filter4: true,
1009 group2__filter5: true
1010 } );
1011
1012 // Both groups have multiple (but not all) items selected, all items are non-muted
1013 assert.deepEqual(
1014 getCurrentItemsMutedState(),
1015 baseMuteState,
1016 'Not all items in the group selected - all items are non-muted'
1017 );
1018
1019 // Select all items in 'fullCoverage' group (group2)
1020 model.toggleFiltersSelected( {
1021 group2__filter6: true
1022 } );
1023
1024 // Group2 (full coverage) has all items selected, all its items are muted
1025 assert.deepEqual(
1026 getCurrentItemsMutedState(),
1027 $.extend( {}, baseMuteState, {
1028 group2__filter4: true,
1029 group2__filter5: true,
1030 group2__filter6: true
1031 } ),
1032 'All items in \'full coverage\' group are selected - all items in the group are muted'
1033 );
1034
1035 // Select all items in non 'fullCoverage' group (group1)
1036 model.toggleFiltersSelected( {
1037 group1__filter3: true
1038 } );
1039
1040 // Group1 (full coverage) has all items selected, no items in it are muted (non full coverage)
1041 assert.deepEqual(
1042 getCurrentItemsMutedState(),
1043 $.extend( {}, baseMuteState, {
1044 group2__filter4: true,
1045 group2__filter5: true,
1046 group2__filter6: true
1047 } ),
1048 'All items in a non \'full coverage\' group are selected - none of the items in the group are muted'
1049 );
1050
1051 // Uncheck an item from each group
1052 model.toggleFiltersSelected( {
1053 group1__filter3: false,
1054 group2__filter5: false
1055 } );
1056 assert.deepEqual(
1057 getCurrentItemsMutedState(),
1058 baseMuteState,
1059 'Not all items in the group are checked - all items are non-muted regardless of group coverage'
1060 );
1061 } );
1062
1063 QUnit.test( 'Filter interaction: conflicts', function ( assert ) {
1064 var definition = [ {
1065 name: 'group1',
1066 title: 'Group 1',
1067 type: 'string_options',
1068 filters: [
1069 {
1070 name: 'filter1',
1071 label: '1',
1072 description: '1',
1073 conflicts: [ { group: 'group2' } ]
1074 },
1075 {
1076 name: 'filter2',
1077 label: '2',
1078 description: '2',
1079 conflicts: [ { group: 'group2', filter: 'filter6' } ]
1080 },
1081 {
1082 name: 'filter3',
1083 label: '3',
1084 description: '3'
1085 }
1086 ]
1087 }, {
1088 name: 'group2',
1089 title: 'Group 2',
1090 type: 'send_unselected_if_any',
1091 conflicts: [ { group: 'group1', filter: 'filter1' } ],
1092 filters: [
1093 {
1094 name: 'filter4',
1095 label: '1',
1096 description: '1'
1097 },
1098 {
1099 name: 'filter5',
1100 label: '5',
1101 description: '5'
1102 },
1103 {
1104 name: 'filter6',
1105 label: '6',
1106 description: '6',
1107 conflicts: [ { group: 'group1', filter: 'filter2' } ]
1108 }
1109 ]
1110 } ],
1111 baseFullState = {
1112 group1__filter1: { selected: false, conflicted: false, included: false },
1113 group1__filter2: { selected: false, conflicted: false, included: false },
1114 group1__filter3: { selected: false, conflicted: false, included: false },
1115 group2__filter4: { selected: false, conflicted: false, included: false },
1116 group2__filter5: { selected: false, conflicted: false, included: false },
1117 group2__filter6: { selected: false, conflicted: false, included: false }
1118 },
1119 model = new mw.rcfilters.dm.FiltersViewModel();
1120
1121 model.initializeFilters( definition );
1122
1123 assert.deepEqual(
1124 model.getFullState(),
1125 baseFullState,
1126 'Initial state: no conflicts because no selections.'
1127 );
1128
1129 // Select a filter that has a conflict with an entire group
1130 model.toggleFiltersSelected( {
1131 group1__filter1: true // conflicts: entire of group 2 ( filter4, filter5, filter6)
1132 } );
1133
1134 model.reassessFilterInteractions( model.getItemByName( 'group1__filter1' ) );
1135
1136 assert.deepEqual(
1137 model.getFullState(),
1138 $.extend( true, {}, baseFullState, {
1139 group1__filter1: { selected: true },
1140 group2__filter4: { conflicted: true },
1141 group2__filter5: { conflicted: true },
1142 group2__filter6: { conflicted: true }
1143 } ),
1144 'Selecting a filter that conflicts with a group sets all the conflicted group items as "conflicted".'
1145 );
1146
1147 // Select one of the conflicts (both filters are now conflicted and selected)
1148 model.toggleFiltersSelected( {
1149 group2__filter4: true // conflicts: filter 1
1150 } );
1151 model.reassessFilterInteractions( model.getItemByName( 'group2__filter4' ) );
1152
1153 assert.deepEqual(
1154 model.getFullState(),
1155 $.extend( true, {}, baseFullState, {
1156 group1__filter1: { selected: true, conflicted: true },
1157 group2__filter4: { selected: true, conflicted: true },
1158 group2__filter5: { conflicted: true },
1159 group2__filter6: { conflicted: true }
1160 } ),
1161 'Selecting a conflicting filter inside a group, sets both sides to conflicted and selected.'
1162 );
1163
1164 // Reset
1165 model = new mw.rcfilters.dm.FiltersViewModel();
1166 model.initializeFilters( definition );
1167
1168 // Select a filter that has a conflict with a specific filter
1169 model.toggleFiltersSelected( {
1170 group1__filter2: true // conflicts: filter6
1171 } );
1172 model.reassessFilterInteractions( model.getItemByName( 'group1__filter2' ) );
1173
1174 assert.deepEqual(
1175 model.getFullState(),
1176 $.extend( true, {}, baseFullState, {
1177 group1__filter2: { selected: true },
1178 group2__filter6: { conflicted: true }
1179 } ),
1180 'Selecting a filter that conflicts with another filter sets the other as "conflicted".'
1181 );
1182
1183 // Select the conflicting filter
1184 model.toggleFiltersSelected( {
1185 group2__filter6: true // conflicts: filter2
1186 } );
1187
1188 model.reassessFilterInteractions( model.getItemByName( 'group2__filter6' ) );
1189
1190 assert.deepEqual(
1191 model.getFullState(),
1192 $.extend( true, {}, baseFullState, {
1193 group1__filter2: { selected: true, conflicted: true },
1194 group2__filter6: { selected: true, conflicted: true },
1195 // This is added to the conflicts because filter6 is part of group2,
1196 // who is in conflict with filter1; note that filter2 also conflicts
1197 // with filter6 which means that filter1 conflicts with filter6 (because it's in group2)
1198 // and also because its **own sibling** (filter2) is **also** in conflict with the
1199 // selected items in group2 (filter6)
1200 group1__filter1: { conflicted: true }
1201 } ),
1202 'Selecting a conflicting filter with an individual filter, sets both sides to conflicted and selected.'
1203 );
1204
1205 // Now choose a non-conflicting filter from the group
1206 model.toggleFiltersSelected( {
1207 group2__filter5: true
1208 } );
1209
1210 model.reassessFilterInteractions( model.getItemByName( 'group2__filter5' ) );
1211
1212 assert.deepEqual(
1213 model.getFullState(),
1214 $.extend( true, {}, baseFullState, {
1215 group1__filter2: { selected: true },
1216 group2__filter6: { selected: true },
1217 group2__filter5: { selected: true }
1218 // Filter6 and filter1 are no longer in conflict because
1219 // filter5, while it is in conflict with filter1, it is
1220 // not in conflict with filter2 - and since filter2 is
1221 // selected, it removes the conflict bidirectionally
1222 } ),
1223 'Selecting a non-conflicting filter within the group of a conflicting filter removes the conflicts.'
1224 );
1225
1226 // Followup on the previous test, unselect filter2 so filter1
1227 // is now the only one selected in its own group, and since
1228 // it is in conflict with the entire of group2, it means
1229 // filter1 is once again conflicted
1230 model.toggleFiltersSelected( {
1231 group1__filter2: false
1232 } );
1233
1234 model.reassessFilterInteractions( model.getItemByName( 'group1__filter2' ) );
1235
1236 assert.deepEqual(
1237 model.getFullState(),
1238 $.extend( true, {}, baseFullState, {
1239 group1__filter1: { conflicted: true },
1240 group2__filter6: { selected: true },
1241 group2__filter5: { selected: true }
1242 } ),
1243 'Unselecting an item that did not conflict returns the conflict state.'
1244 );
1245
1246 // Followup #2: Now actually select filter1, and make everything conflicted
1247 model.toggleFiltersSelected( {
1248 group1__filter1: true
1249 } );
1250
1251 model.reassessFilterInteractions( model.getItemByName( 'group1__filter1' ) );
1252
1253 assert.deepEqual(
1254 model.getFullState(),
1255 $.extend( true, {}, baseFullState, {
1256 group1__filter1: { selected: true, conflicted: true },
1257 group2__filter6: { selected: true, conflicted: true },
1258 group2__filter5: { selected: true, conflicted: true },
1259 group2__filter4: { conflicted: true } // Not selected but conflicted because it's in group2
1260 } ),
1261 'Selecting an item that conflicts with a whole group makes all selections in that group conflicted.'
1262 );
1263
1264 /* Simple case */
1265 // Reset
1266 model = new mw.rcfilters.dm.FiltersViewModel();
1267 model.initializeFilters( definition );
1268
1269 // Select a filter that has a conflict with a specific filter
1270 model.toggleFiltersSelected( {
1271 group1__filter2: true // conflicts: filter6
1272 } );
1273
1274 model.reassessFilterInteractions( model.getItemByName( 'group1__filter2' ) );
1275
1276 assert.deepEqual(
1277 model.getFullState(),
1278 $.extend( true, {}, baseFullState, {
1279 group1__filter2: { selected: true },
1280 group2__filter6: { conflicted: true }
1281 } ),
1282 'Simple case: Selecting a filter that conflicts with another filter sets the other as "conflicted".'
1283 );
1284
1285 model.toggleFiltersSelected( {
1286 group1__filter3: true // conflicts: filter6
1287 } );
1288
1289 model.reassessFilterInteractions( model.getItemByName( 'group1__filter3' ) );
1290
1291 assert.deepEqual(
1292 model.getFullState(),
1293 $.extend( true, {}, baseFullState, {
1294 group1__filter2: { selected: true },
1295 group1__filter3: { selected: true }
1296 } ),
1297 'Simple case: Selecting a filter that is not in conflict removes the conflict.'
1298 );
1299
1300 } );
1301
1302 QUnit.test( 'Filter highlights', function ( assert ) {
1303 var definition = [ {
1304 name: 'group1',
1305 title: 'Group 1',
1306 type: 'string_options',
1307 filters: [
1308 { name: 'filter1', cssClass: 'class1', label: '1', description: '1' },
1309 { name: 'filter2', cssClass: 'class2', label: '2', description: '2' },
1310 { name: 'filter3', cssClass: 'class3', label: '3', description: '3' },
1311 { name: 'filter4', cssClass: 'class4', label: '4', description: '4' },
1312 { name: 'filter5', cssClass: 'class5', label: '5', description: '5' },
1313 { name: 'filter6', label: '6', description: '6' }
1314 ]
1315 } ],
1316 model = new mw.rcfilters.dm.FiltersViewModel();
1317
1318 model.initializeFilters( definition );
1319
1320 assert.ok(
1321 !model.isHighlightEnabled(),
1322 'Initially, highlight is disabled.'
1323 );
1324
1325 model.toggleHighlight( true );
1326 assert.ok(
1327 model.isHighlightEnabled(),
1328 'Highlight is enabled on toggle.'
1329 );
1330
1331 model.setHighlightColor( 'group1__filter1', 'color1' );
1332 model.setHighlightColor( 'group1__filter2', 'color2' );
1333
1334 assert.deepEqual(
1335 model.getHighlightedItems().map( function ( item ) {
1336 return item.getName();
1337 } ),
1338 [
1339 'group1__filter1',
1340 'group1__filter2'
1341 ],
1342 'Highlighted items are highlighted.'
1343 );
1344
1345 assert.equal(
1346 model.getItemByName( 'group1__filter1' ).getHighlightColor(),
1347 'color1',
1348 'Item highlight color is set.'
1349 );
1350
1351 model.setHighlightColor( 'group1__filter1', 'color1changed' );
1352 assert.equal(
1353 model.getItemByName( 'group1__filter1' ).getHighlightColor(),
1354 'color1changed',
1355 'Item highlight color is changed on setHighlightColor.'
1356 );
1357
1358 model.clearHighlightColor( 'group1__filter1' );
1359 assert.deepEqual(
1360 model.getHighlightedItems().map( function ( item ) {
1361 return item.getName();
1362 } ),
1363 [
1364 'group1__filter2'
1365 ],
1366 'Clear highlight from an item results in the item no longer being highlighted.'
1367 );
1368
1369 // Reset
1370 model = new mw.rcfilters.dm.FiltersViewModel();
1371 model.initializeFilters( definition );
1372
1373 model.setHighlightColor( 'group1__filter1', 'color1' );
1374 model.setHighlightColor( 'group1__filter2', 'color2' );
1375 model.setHighlightColor( 'group1__filter3', 'color3' );
1376
1377 assert.deepEqual(
1378 model.getHighlightedItems().map( function ( item ) {
1379 return item.getName();
1380 } ),
1381 [
1382 'group1__filter1',
1383 'group1__filter2',
1384 'group1__filter3'
1385 ],
1386 'Even if highlights are not enabled, the items remember their highlight state'
1387 // NOTE: When actually displaying the highlights, the UI checks whether
1388 // highlighting is generally active and then goes over the highlighted
1389 // items. The item models, however, and the view model in general, still
1390 // retains the knowledge about which filters have different colors, so we
1391 // can seamlessly return to the colors the user previously chose if they
1392 // reapply highlights.
1393 );
1394
1395 // Reset
1396 model = new mw.rcfilters.dm.FiltersViewModel();
1397 model.initializeFilters( definition );
1398
1399 model.setHighlightColor( 'group1__filter1', 'color1' );
1400 model.setHighlightColor( 'group1__filter6', 'color6' );
1401
1402 assert.deepEqual(
1403 model.getHighlightedItems().map( function ( item ) {
1404 return item.getName();
1405 } ),
1406 [
1407 'group1__filter1'
1408 ],
1409 'Items without a specified class identifier are not highlighted.'
1410 );
1411 } );
1412 }( mediaWiki, jQuery ) );