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