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