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