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