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