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