1 /* eslint-disable camelcase */
3 var filterDefinition
= [ {
5 type
: 'send_unselected_if_any',
8 name
: 'filter1', label
: 'group1filter1-label', description
: 'group1filter1-desc',
10 conflicts
: [ { group
: 'group2' } ],
23 name
: 'filter2', label
: 'group1filter2-label', description
: 'group1filter2-desc',
24 conflicts
: [ { group
: 'group2', filter
: 'filter6' } ],
32 { name
: 'filter3', label
: 'group1filter3-label', description
: 'group1filter3-desc', default: true }
36 type
: 'send_unselected_if_any',
38 conflicts
: [ { group
: 'group1', filter
: 'filter1' } ],
40 { name
: 'filter4', label
: 'group2filter4-label', description
: 'group2filter4-desc' },
41 { name
: 'filter5', label
: 'group2filter5-label', description
: 'group2filter5-desc', default: true },
43 name
: 'filter6', label
: 'group2filter6-label', description
: 'group2filter6-desc',
44 conflicts
: [ { group
: 'group1', filter
: 'filter2' } ]
49 type
: 'string_options',
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' }
59 type
: 'single_option',
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' }
68 type
: 'single_option',
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' }
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 }
85 type
: 'single_option',
87 default: 'group7option2',
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' }
101 type
: 'string_options',
104 { name
: 0, label
: 'Main' },
105 { name
: 1, label
: 'Talk' },
106 { name
: 2, label
: 'User' },
107 { name
: 3, label
: 'User talk' }
112 defaultParameters
= {
125 group7
: 'group7option2',
128 baseParamRepresentation
= {
141 group7
: 'group7option2',
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,
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 }
201 QUnit
.module( 'mediawiki.rcfilters - FiltersViewModel', QUnit
.newMwEnvironment( {
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',
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'
218 wgStructuredChangeFiltersEnableExperimentalViews
: true
222 QUnit
.test( 'Setting up filters', function ( assert
) {
223 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
225 model
.initializeFilters( filterDefinition
, viewsDefinition
);
227 // Test that all items were created
229 Object
.keys( baseFilterRepresentation
).every( function ( filterName
) {
230 return model
.getItemByName( filterName
) instanceof mw
.rcfilters
.dm
.FilterItem
;
232 'Filters instantiated and stored correctly'
236 model
.getSelectedState(),
237 baseFilterRepresentation
,
238 'Initial state of filters'
241 model
.toggleFiltersSelected( {
242 group1__filter1
: true,
243 group2__filter5
: true,
244 group3__filter7
: true
247 model
.getSelectedState(),
248 $.extend( true, {}, baseFilterRepresentation
, {
249 group1__filter1
: true,
250 group2__filter5
: true,
251 group3__filter7
: true
253 'Updating filter states correctly'
257 QUnit
.test( 'Default filters', function ( assert
) {
258 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
260 model
.initializeFilters( filterDefinition
, viewsDefinition
);
262 // Empty query = only default values
264 model
.getDefaultParams(),
266 'Default parameters are stored properly per filter and group'
269 // Change sticky filter
270 model
.toggleFiltersSelected( {
271 group7__group7option1
: true
274 // Make sure defaults have changed
276 model
.getDefaultParams(),
277 $.extend( true, {}, defaultParameters
, {
278 group7
: 'group7option1'
280 'Default parameters are stored properly per filter and group'
284 !model
.areDefaultFiltersEmpty(),
285 'Check if default filters are empty when defaults exist'
288 // Reset on special filter object that has no defaults
289 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
290 model
.initializeFilters(
293 type
: 'send_unselected_if_any',
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' }
303 model
.areDefaultFiltersEmpty(),
304 'Check if default filters are empty when defaults do not exist'
308 QUnit
.test( 'Finding matching filters', function ( assert
) {
314 group1
: [ 'group1__filter1', 'group1__filter2', 'group1__filter3' ],
315 group2
: [ 'group2__filter4', 'group2__filter5' ]
317 reason
: 'Finds filters starting with the query string'
322 group2
: [ 'group2__filter4', 'group2__filter5', 'group2__filter6' ]
324 reason
: 'Finds filters containing the query string in their description'
329 group1
: [ 'group1__filter1', 'group1__filter2' ],
330 group2
: [ 'group2__filter4' ]
332 reason
: 'Finds filters containing the query string in their group title'
337 namespace: [ 'namespace__0' ]
339 reason
: 'Finds item in view when a prefix is used'
344 reason
: 'Finds no results if using namespaces prefix (:) to search for filter title'
347 model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
348 extractNames = function ( matches
) {
350 Object
.keys( matches
).forEach( function ( groupName
) {
351 result
[ groupName
] = matches
[ groupName
].map( function ( item
) {
352 return item
.getName();
358 model
.initializeFilters( filterDefinition
, viewsDefinition
);
360 testCases
.forEach( function ( testCase
) {
361 matches
= model
.findMatches( testCase
.query
);
363 extractNames( matches
),
364 testCase
.expectedMatches
,
369 matches
= model
.findMatches( 'foo' );
371 $.isEmptyObject( matches
),
372 'findMatches returns an empty object when no results found'
376 QUnit
.test( 'getParametersFromFilters', function ( assert
) {
377 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
379 model
.initializeFilters( filterDefinition
, viewsDefinition
);
381 // Starting with all filters unselected
383 model
.getParametersFromFilters(),
384 baseParamRepresentation
,
385 'Unselected filters return all parameters falsey or \'\'.'
389 model
.toggleFiltersSelected( {
390 group1__filter1
: true
392 // Only one filter in one group
394 model
.getParametersFromFilters(),
395 $.extend( true, {}, baseParamRepresentation
, {
396 // Group 1 (one selected, the others are true)
400 'One filter in one "send_unselected_if_any" group returns the other parameters truthy.'
404 model
.toggleFiltersSelected( {
405 group1__filter1
: true,
406 group1__filter2
: true
408 // Two selected filters in one group
410 model
.getParametersFromFilters(),
411 $.extend( true, {}, baseParamRepresentation
, {
412 // Group 1 (two selected, the other is true)
415 'Two filters in one "send_unselected_if_any" group returns the other parameters truthy.'
419 model
.toggleFiltersSelected( {
420 group1__filter1
: true,
421 group1__filter2
: true,
422 group1__filter3
: true
424 // All filters of the group are selected == this is the same as not selecting any
426 model
.getParametersFromFilters(),
427 baseParamRepresentation
,
428 'All filters selected in one "send_unselected_if_any" group returns all parameters falsy.'
431 // Select 1 filter from string_options
432 model
.toggleFiltersSelected( {
433 group3__filter7
: true,
434 group3__filter8
: false,
435 group3__filter9
: false
437 // All filters of the group are selected == this is the same as not selecting any
439 model
.getParametersFromFilters(),
440 $.extend( true, {}, baseParamRepresentation
, {
443 'One filter selected in "string_option" group returns that filter in the value.'
446 // Select 2 filters from string_options
447 model
.toggleFiltersSelected( {
448 group3__filter7
: true,
449 group3__filter8
: true,
450 group3__filter9
: false
452 // All filters of the group are selected == this is the same as not selecting any
454 model
.getParametersFromFilters(),
455 $.extend( true, {}, baseParamRepresentation
, {
456 group3
: 'filter7,filter8'
458 'Two filters selected in "string_option" group returns those filters in the value.'
461 // Select 3 filters from string_options
462 model
.toggleFiltersSelected( {
463 group3__filter7
: true,
464 group3__filter8
: true,
465 group3__filter9
: true
467 // All filters of the group are selected == this is the same as not selecting any
469 model
.getParametersFromFilters(),
470 $.extend( true, {}, baseParamRepresentation
, {
473 'All filters selected in "string_option" group returns \'all\'.'
477 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
478 model
.initializeFilters( filterDefinition
, viewsDefinition
);
480 // Select an option from single_option group
481 model
.toggleFiltersSelected( {
482 group4__option2
: true
484 // All filters of the group are selected == this is the same as not selecting any
486 model
.getParametersFromFilters(),
487 $.extend( true, {}, baseParamRepresentation
, {
490 'Selecting an option from "single_option" group returns that option as a value.'
493 // Select a different option from single_option group
494 model
.toggleFiltersSelected( {
495 group4__option3
: true
497 // All filters of the group are selected == this is the same as not selecting any
499 model
.getParametersFromFilters(),
500 $.extend( true, {}, baseParamRepresentation
, {
503 'Selecting a different option from "single_option" group changes the selection.'
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
512 model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
516 type
: 'send_unselected_if_any',
518 { name
: 'hidefilter1', label
: 'Hide filter 1', description
: '' },
519 { name
: 'hidefilter2', label
: 'Hide filter 2', description
: '' },
520 { name
: 'hidefilter3', label
: 'Hide filter 3', description
: '' }
525 type
: 'send_unselected_if_any',
527 { name
: 'hidefilter4', label
: 'Hide filter 4', description
: '' },
528 { name
: 'hidefilter5', label
: 'Hide filter 5', description
: '' },
529 { name
: 'hidefilter6', label
: 'Hide filter 6', description
: '' }
534 type
: 'string_options',
537 { name
: 'filter7', label
: 'Hide filter 7', description
: '' },
538 { name
: 'filter8', label
: 'Hide filter 8', description
: '' },
539 { name
: 'filter9', label
: 'Hide filter 9', description
: '' }
544 type
: 'single_option',
546 { name
: 'filter10', label
: 'Hide filter 10', description
: '' },
547 { name
: 'filter11', label
: 'Hide filter 11', description
: '' },
548 { name
: 'filter12', label
: 'Hide filter 12', description
: '' }
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.'
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
577 expected
: $.extend( true, {}, baseResult
, {
578 // Group 1 (two selected, the others are true)
580 // Group 3 (two selected)
581 group3
: 'filter7,filter8'
583 msg
: 'Given an explicit (complete) filter state object, the result is the same as if the object given represented the model state.'
586 // This is mocking case above
587 // - 'One filter in one "send_unselected_if_any" group returns the other parameters truthy.'
589 group1__hidefilter1
: 1
591 expected
: $.extend( true, {}, baseResult
, {
592 // Group 1 (one selected, the others are true)
596 msg
: 'Given an explicit (incomplete) filter state object, the result is the same as if the object give represented the model state.'
600 group4__filter10
: true
602 expected
: $.extend( true, {}, baseResult
, {
605 msg
: 'Given a single value for "single_option" that option is represented in the result.'
609 group4__filter10
: true,
610 group4__filter11
: true
612 expected
: $.extend( true, {}, baseResult
, {
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.'
619 expected
: baseResult
,
620 msg
: 'Given an explicit empty object, the result is all filters set to their falsey unselected value.'
624 model
.initializeFilters( definition
);
625 // Store original state
626 originalState
= model
.getSelectedState();
629 cases
.forEach( function ( test
) {
631 model
.getParametersFromFilters( test
.input
),
637 // After doing the above tests, make sure the actual state
638 // of the filter stayed the same
640 model
.getSelectedState(),
642 'Running the method with external definition to parse does not actually change the state of the model'
646 QUnit
.test( 'getFiltersFromParameters', function ( assert
) {
647 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
649 model
.initializeFilters( filterDefinition
, viewsDefinition
);
651 // Empty query = only default values
653 model
.getFiltersFromParameters( {} ),
654 baseFilterRepresentation
,
655 'Empty parameter query results in an object representing all filters set to their base state'
659 model
.getFiltersFromParameters( {
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"
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)'
671 model
.getFiltersFromParameters( {
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"
681 'All paremeters in the same \'send_unselected_if_any\' group false is equivalent to none are truthy (checked) in the interface'
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( {
699 model
.toggleFiltersSelected(
700 model
.getFiltersFromParameters( {
705 // The result here is ignoring the first toggleFiltersSelected call
707 model
.getSelectedState(),
708 $.extend( {}, baseFilterRepresentation
, {
709 group2__filter4
: true,
710 group2__filter5
: true,
711 group2__filter6
: false
713 'getFiltersFromParameters does not care about previous or existing state.'
717 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
718 model
.initializeFilters( filterDefinition
, viewsDefinition
);
720 model
.toggleFiltersSelected(
721 model
.getFiltersFromParameters( {
726 model
.getSelectedState(),
727 $.extend( {}, baseFilterRepresentation
, {
728 group3__filter7
: true,
729 group3__filter8
: false,
730 group3__filter9
: false
732 'A \'string_options\' parameter containing 1 value, results in the corresponding filter as checked'
735 model
.toggleFiltersSelected(
736 model
.getFiltersFromParameters( {
737 group3
: 'filter7,filter8'
741 model
.getSelectedState(),
742 $.extend( {}, baseFilterRepresentation
, {
743 group3__filter7
: true,
744 group3__filter8
: true,
745 group3__filter9
: false
747 'A \'string_options\' parameter containing 2 values, results in both corresponding filters as checked'
750 model
.toggleFiltersSelected(
751 model
.getFiltersFromParameters( {
752 group3
: 'filter7,filter8,filter9'
756 model
.getSelectedState(),
757 $.extend( {}, baseFilterRepresentation
, {
758 group3__filter7
: true,
759 group3__filter8
: true,
760 group3__filter9
: true
762 'A \'string_options\' parameter containing all values, results in all filters of the group as checked.'
765 model
.toggleFiltersSelected(
766 model
.getFiltersFromParameters( {
767 group3
: 'filter7,all,filter9'
771 model
.getSelectedState(),
772 $.extend( {}, baseFilterRepresentation
, {
773 group3__filter7
: true,
774 group3__filter8
: true,
775 group3__filter9
: true
777 'A \'string_options\' parameter containing the value \'all\', results in all filters of the group as checked.'
780 model
.toggleFiltersSelected(
781 model
.getFiltersFromParameters( {
782 group3
: 'filter7,foo,filter9'
786 model
.getSelectedState(),
787 $.extend( {}, baseFilterRepresentation
, {
788 group3__filter7
: true,
789 group3__filter8
: false,
790 group3__filter9
: true
792 'A \'string_options\' parameter containing an invalid value, results in the invalid value ignored and the valid corresponding filters checked.'
795 model
.toggleFiltersSelected(
796 model
.getFiltersFromParameters( {
801 model
.getSelectedState(),
802 $.extend( {}, baseFilterRepresentation
, {
803 group4__option1
: true,
804 group4__option2
: false
806 'A \'single_option\' parameter reflects a single selected value.'
810 model
.getFiltersFromParameters( {
811 group4
: 'option1,option2'
813 baseFilterRepresentation
,
814 'An invalid \'single_option\' parameter is ignored.'
817 // Change to one value
818 model
.toggleFiltersSelected(
819 model
.getFiltersFromParameters( {
823 // Change again to another value
824 model
.toggleFiltersSelected(
825 model
.getFiltersFromParameters( {
830 model
.getSelectedState(),
831 $.extend( {}, baseFilterRepresentation
, {
832 group4__option2
: true
834 'A \'single_option\' parameter always reflects the latest selected value.'
838 QUnit
.test( 'sanitizeStringOptionGroup', function ( assert
) {
839 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
841 model
.initializeFilters( filterDefinition
, viewsDefinition
);
844 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'filter1', 'filter2' ] ),
845 [ 'filter1', 'filter2' ],
846 'Remove duplicate values'
850 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'foo', 'filter2' ] ),
851 [ 'filter1', 'filter2' ],
852 'Remove invalid values'
856 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'all', 'filter2' ] ),
858 'If any value is "all", the only value is "all".'
862 QUnit
.test( 'Filter interaction: subsets', function ( assert
) {
863 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
865 model
.initializeFilters( filterDefinition
, viewsDefinition
);
867 // Select a filter that has subset with another filter
868 model
.toggleFiltersSelected( {
869 group1__filter1
: true
872 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
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 }
884 'Filters with subsets are represented in the model.'
887 // Select another filter that has a subset with the same previous filter
888 model
.toggleFiltersSelected( {
889 group1__filter2
: true
891 model
.reassessFilterInteractions( model
.getItemByName( 'filter2' ) );
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 }
901 'Filters that have multiple subsets are represented.'
904 // Remove one filter (but leave the other) that affects filter3
905 model
.toggleFiltersSelected( {
906 group1__filter1
: false
908 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
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 }
917 'Removing a filter only un-includes its subset if there is no other filter affecting.'
920 model
.toggleFiltersSelected( {
921 group1__filter2
: false
923 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
925 model
.getFullState(),
927 'Removing all supersets also un-includes the subsets.'
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();
937 // This is the logic inside the capsule widget
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()
945 getCurrentItemsMutedState = function () {
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' )
956 group1__filter1
: false,
957 group1__filter2
: false,
958 group1__filter3
: false,
959 group2__filter4
: false,
960 group2__filter5
: false,
961 group2__filter6
: false
964 model
.initializeFilters( filterDefinition
, viewsDefinition
);
966 // Starting state, no selection, all items are non-muted
968 getCurrentItemsMutedState(),
970 'No selection - all items are non-muted'
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
981 // Both groups have multiple (but not all) items selected, all items are non-muted
983 getCurrentItemsMutedState(),
985 'Not all items in the group selected - all items are non-muted'
988 // Select all items in 'fullCoverage' group (group2)
989 model
.toggleFiltersSelected( {
990 group2__filter6
: true
993 // Group2 (full coverage) has all items selected, all its items are muted
995 getCurrentItemsMutedState(),
996 $.extend( {}, baseMuteState
, {
997 group2__filter4
: true,
998 group2__filter5
: true,
999 group2__filter6
: true
1001 'All items in \'full coverage\' group are selected - all items in the group are muted'
1004 // Select all items in non 'fullCoverage' group (group1)
1005 model
.toggleFiltersSelected( {
1006 group1__filter3
: true
1009 // Group1 (full coverage) has all items selected, no items in it are muted (non full coverage)
1011 getCurrentItemsMutedState(),
1012 $.extend( {}, baseMuteState
, {
1013 group2__filter4
: true,
1014 group2__filter5
: true,
1015 group2__filter6
: true
1017 'All items in a non \'full coverage\' group are selected - none of the items in the group are muted'
1020 // Uncheck an item from each group
1021 model
.toggleFiltersSelected( {
1022 group1__filter3
: false,
1023 group2__filter5
: false
1026 getCurrentItemsMutedState(),
1028 'Not all items in the group are checked - all items are non-muted regardless of group coverage'
1032 QUnit
.test( 'Filter interaction: conflicts', function ( assert
) {
1033 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1035 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1038 model
.getFullState(),
1039 baseFullFilterState
,
1040 'Initial state: no conflicts because no selections.'
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)
1048 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
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 }
1061 'Selecting a filter that conflicts with a group sets all the conflicted group items as "conflicted".'
1064 // Select one of the conflicts (both filters are now conflicted and selected)
1065 model
.toggleFiltersSelected( {
1066 group2__filter4
: true // conflicts: filter 1
1068 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter4' ) );
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 }
1081 'Selecting a conflicting filter inside a group, sets both sides to conflicted and selected.'
1085 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1086 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1088 // Select a filter that has a conflict with a specific filter
1089 model
.toggleFiltersSelected( {
1090 group1__filter2
: true // conflicts: filter6
1092 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
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 }
1102 'Selecting a filter that conflicts with another filter sets the other as "conflicted".'
1105 // Select the conflicting filter
1106 model
.toggleFiltersSelected( {
1107 group2__filter6
: true // conflicts: filter2
1110 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter6' ) );
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 },
1124 // Subsets are affected by the selection
1125 group1__filter3
: { included
: true }
1127 'Selecting a conflicting filter with an individual filter, sets both sides to conflicted and selected.'
1130 // Now choose a non-conflicting filter from the group
1131 model
.toggleFiltersSelected( {
1132 group2__filter5
: true
1135 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter5' ) );
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
1148 // Subsets are affected by the selection
1149 group1__filter3
: { included
: true }
1151 'Selecting a non-conflicting filter within the group of a conflicting filter removes the conflicts.'
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
1162 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1165 model
.getFullState(),
1166 $.extend( true, {}, baseFullFilterState
, {
1167 group1__filter1
: { conflicted
: true },
1168 group2__filter6
: { selected
: true },
1169 group2__filter5
: { selected
: true }
1171 'Unselecting an item that did not conflict returns the conflict state.'
1174 // Followup #2: Now actually select filter1, and make everything conflicted
1175 model
.toggleFiltersSelected( {
1176 group1__filter1
: true
1179 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
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 }
1192 'Selecting an item that conflicts with a whole group makes all selections in that group conflicted.'
1197 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1198 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1200 // Select a filter that has a conflict with a specific filter
1201 model
.toggleFiltersSelected( {
1202 group1__filter2
: true // conflicts: filter6
1205 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
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 }
1215 'Simple case: Selecting a filter that conflicts with another filter sets the other as "conflicted".'
1218 model
.toggleFiltersSelected( {
1219 group1__filter3
: true // conflicts: filter6
1222 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter3' ) );
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 }
1231 'Simple case: Selecting a filter that is not in conflict removes the conflict.'
1235 QUnit
.test( 'Filter highlights', function ( assert
) {
1236 // We are using a different (smaller) definition here than the global one
1237 var definition
= [ {
1240 type
: 'string_options',
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' }
1250 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1252 model
.initializeFilters( definition
);
1255 !model
.isHighlightEnabled(),
1256 'Initially, highlight is disabled.'
1259 model
.toggleHighlight( true );
1261 model
.isHighlightEnabled(),
1262 'Highlight is enabled on toggle.'
1265 model
.setHighlightColor( 'group1__filter1', 'color1' );
1266 model
.setHighlightColor( 'group1__filter2', 'color2' );
1269 model
.getHighlightedItems().map( function ( item
) {
1270 return item
.getName();
1276 'Highlighted items are highlighted.'
1280 model
.getItemByName( 'group1__filter1' ).getHighlightColor(),
1282 'Item highlight color is set.'
1285 model
.setHighlightColor( 'group1__filter1', 'color1changed' );
1287 model
.getItemByName( 'group1__filter1' ).getHighlightColor(),
1289 'Item highlight color is changed on setHighlightColor.'
1292 model
.clearHighlightColor( 'group1__filter1' );
1294 model
.getHighlightedItems().map( function ( item
) {
1295 return item
.getName();
1300 'Clear highlight from an item results in the item no longer being highlighted.'
1304 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1305 model
.initializeFilters( definition
);
1307 model
.setHighlightColor( 'group1__filter1', 'color1' );
1308 model
.setHighlightColor( 'group1__filter2', 'color2' );
1309 model
.setHighlightColor( 'group1__filter3', 'color3' );
1312 model
.getHighlightedItems().map( function ( item
) {
1313 return item
.getName();
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.
1330 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1331 model
.initializeFilters( definition
);
1333 model
.setHighlightColor( 'group1__filter1', 'color1' );
1334 model
.setHighlightColor( 'group1__filter6', 'color6' );
1337 model
.getHighlightedItems().map( function ( item
) {
1338 return item
.getName();
1343 'Items without a specified class identifier are not highlighted.'
1346 }( mediaWiki
, jQuery
) );