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