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