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