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