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