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 QUnit
.test( 'Finding matching filters', function ( assert
) {
290 group1
: [ 'group1__filter1', 'group1__filter2', 'group1__filter3' ],
291 group2
: [ 'group2__filter4', 'group2__filter5' ]
293 reason
: 'Finds filters starting with the query string'
298 group2
: [ 'group2__filter4', 'group2__filter5', 'group2__filter6' ]
300 reason
: 'Finds filters containing the query string in their description'
305 group1
: [ 'group1__filter1', 'group1__filter2' ],
306 group2
: [ 'group2__filter4' ]
308 reason
: 'Finds filters containing the query string in their group title'
313 namespace: [ 'namespace__0' ]
315 reason
: 'Finds item in view when a prefix is used'
320 reason
: 'Finds no results if using namespaces prefix (:) to search for filter title'
323 model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
324 extractNames = function ( matches
) {
326 Object
.keys( matches
).forEach( function ( groupName
) {
327 result
[ groupName
] = matches
[ groupName
].map( function ( item
) {
328 return item
.getName();
334 model
.initializeFilters( filterDefinition
, viewsDefinition
);
336 testCases
.forEach( function ( testCase
) {
337 matches
= model
.findMatches( testCase
.query
);
339 extractNames( matches
),
340 testCase
.expectedMatches
,
345 matches
= model
.findMatches( 'foo' );
347 $.isEmptyObject( matches
),
348 'findMatches returns an empty object when no results found'
352 QUnit
.test( 'getParametersFromFilters', function ( assert
) {
353 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
355 model
.initializeFilters( filterDefinition
, viewsDefinition
);
357 // Starting with all filters unselected
359 model
.getParametersFromFilters(),
360 baseParamRepresentation
,
361 'Unselected filters return all parameters falsey or \'\'.'
365 model
.toggleFiltersSelected( {
366 group1__filter1
: true
368 // Only one filter in one group
370 model
.getParametersFromFilters(),
371 $.extend( true, {}, baseParamRepresentation
, {
372 // Group 1 (one selected, the others are true)
376 'One filter in one "send_unselected_if_any" group returns the other parameters truthy.'
380 model
.toggleFiltersSelected( {
381 group1__filter1
: true,
382 group1__filter2
: true
384 // Two selected filters in one group
386 model
.getParametersFromFilters(),
387 $.extend( true, {}, baseParamRepresentation
, {
388 // Group 1 (two selected, the other is true)
391 'Two filters in one "send_unselected_if_any" group returns the other parameters truthy.'
395 model
.toggleFiltersSelected( {
396 group1__filter1
: true,
397 group1__filter2
: true,
398 group1__filter3
: true
400 // All filters of the group are selected == this is the same as not selecting any
402 model
.getParametersFromFilters(),
403 baseParamRepresentation
,
404 'All filters selected in one "send_unselected_if_any" group returns all parameters falsy.'
407 // Select 1 filter from string_options
408 model
.toggleFiltersSelected( {
409 group3__filter7
: true,
410 group3__filter8
: false,
411 group3__filter9
: false
413 // All filters of the group are selected == this is the same as not selecting any
415 model
.getParametersFromFilters(),
416 $.extend( true, {}, baseParamRepresentation
, {
419 'One filter selected in "string_option" group returns that filter in the value.'
422 // Select 2 filters from string_options
423 model
.toggleFiltersSelected( {
424 group3__filter7
: true,
425 group3__filter8
: true,
426 group3__filter9
: false
428 // All filters of the group are selected == this is the same as not selecting any
430 model
.getParametersFromFilters(),
431 $.extend( true, {}, baseParamRepresentation
, {
432 group3
: 'filter7,filter8'
434 'Two filters selected in "string_option" group returns those filters in the value.'
437 // Select 3 filters from string_options
438 model
.toggleFiltersSelected( {
439 group3__filter7
: true,
440 group3__filter8
: true,
441 group3__filter9
: true
443 // All filters of the group are selected == this is the same as not selecting any
445 model
.getParametersFromFilters(),
446 $.extend( true, {}, baseParamRepresentation
, {
449 'All filters selected in "string_option" group returns \'all\'.'
453 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
454 model
.initializeFilters( filterDefinition
, viewsDefinition
);
456 // Select an option from single_option group
457 model
.toggleFiltersSelected( {
458 group4__option2
: true
460 // All filters of the group are selected == this is the same as not selecting any
462 model
.getParametersFromFilters(),
463 $.extend( true, {}, baseParamRepresentation
, {
466 'Selecting an option from "single_option" group returns that option as a value.'
469 // Select a different option from single_option group
470 model
.toggleFiltersSelected( {
471 group4__option3
: true
473 // All filters of the group are selected == this is the same as not selecting any
475 model
.getParametersFromFilters(),
476 $.extend( true, {}, baseParamRepresentation
, {
479 'Selecting a different option from "single_option" group changes the selection.'
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
488 model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
492 type
: 'send_unselected_if_any',
494 { name
: 'hidefilter1', label
: 'Hide filter 1', description
: '' },
495 { name
: 'hidefilter2', label
: 'Hide filter 2', description
: '' },
496 { name
: 'hidefilter3', label
: 'Hide filter 3', description
: '' }
501 type
: 'send_unselected_if_any',
503 { name
: 'hidefilter4', label
: 'Hide filter 4', description
: '' },
504 { name
: 'hidefilter5', label
: 'Hide filter 5', description
: '' },
505 { name
: 'hidefilter6', label
: 'Hide filter 6', description
: '' }
510 type
: 'string_options',
513 { name
: 'filter7', label
: 'Hide filter 7', description
: '' },
514 { name
: 'filter8', label
: 'Hide filter 8', description
: '' },
515 { name
: 'filter9', label
: 'Hide filter 9', description
: '' }
520 type
: 'single_option',
522 { name
: 'filter10', label
: 'Hide filter 10', description
: '' },
523 { name
: 'filter11', label
: 'Hide filter 11', description
: '' },
524 { name
: 'filter12', label
: 'Hide filter 12', description
: '' }
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.'
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
553 expected
: $.extend( true, {}, baseResult
, {
554 // Group 1 (two selected, the others are true)
556 // Group 3 (two selected)
557 group3
: 'filter7,filter8'
559 msg
: 'Given an explicit (complete) filter state object, the result is the same as if the object given represented the model state.'
562 // This is mocking case above
563 // - 'One filter in one "send_unselected_if_any" group returns the other parameters truthy.'
565 group1__hidefilter1
: 1
567 expected
: $.extend( true, {}, baseResult
, {
568 // Group 1 (one selected, the others are true)
572 msg
: 'Given an explicit (incomplete) filter state object, the result is the same as if the object give represented the model state.'
576 group4__filter10
: true
578 expected
: $.extend( true, {}, baseResult
, {
581 msg
: 'Given a single value for "single_option" that option is represented in the result.'
585 group4__filter10
: true,
586 group4__filter11
: true
588 expected
: $.extend( true, {}, baseResult
, {
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.'
595 expected
: baseResult
,
596 msg
: 'Given an explicit empty object, the result is all filters set to their falsey unselected value.'
600 model
.initializeFilters( definition
);
601 // Store original state
602 originalState
= model
.getSelectedState();
605 cases
.forEach( function ( test
) {
607 model
.getParametersFromFilters( test
.input
),
613 // After doing the above tests, make sure the actual state
614 // of the filter stayed the same
616 model
.getSelectedState(),
618 'Running the method with external definition to parse does not actually change the state of the model'
622 QUnit
.test( 'getFiltersFromParameters', function ( assert
) {
623 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
625 model
.initializeFilters( filterDefinition
, viewsDefinition
);
627 // Empty query = only default values
629 model
.getFiltersFromParameters( {} ),
630 baseFilterRepresentation
,
631 'Empty parameter query results in an object representing all filters set to their base state'
635 model
.getFiltersFromParameters( {
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"
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)'
647 model
.getFiltersFromParameters( {
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"
657 'All paremeters in the same \'send_unselected_if_any\' group false is equivalent to none are truthy (checked) in the interface'
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( {
675 model
.toggleFiltersSelected(
676 model
.getFiltersFromParameters( {
681 // The result here is ignoring the first toggleFiltersSelected call
683 model
.getSelectedState(),
684 $.extend( {}, baseFilterRepresentation
, {
685 group2__filter4
: true,
686 group2__filter5
: true,
687 group2__filter6
: false
689 'getFiltersFromParameters does not care about previous or existing state.'
693 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
694 model
.initializeFilters( filterDefinition
, viewsDefinition
);
696 model
.toggleFiltersSelected(
697 model
.getFiltersFromParameters( {
702 model
.getSelectedState(),
703 $.extend( {}, baseFilterRepresentation
, {
704 group3__filter7
: true,
705 group3__filter8
: false,
706 group3__filter9
: false
708 'A \'string_options\' parameter containing 1 value, results in the corresponding filter as checked'
711 model
.toggleFiltersSelected(
712 model
.getFiltersFromParameters( {
713 group3
: 'filter7,filter8'
717 model
.getSelectedState(),
718 $.extend( {}, baseFilterRepresentation
, {
719 group3__filter7
: true,
720 group3__filter8
: true,
721 group3__filter9
: false
723 'A \'string_options\' parameter containing 2 values, results in both corresponding filters as checked'
726 model
.toggleFiltersSelected(
727 model
.getFiltersFromParameters( {
728 group3
: 'filter7,filter8,filter9'
732 model
.getSelectedState(),
733 $.extend( {}, baseFilterRepresentation
, {
734 group3__filter7
: true,
735 group3__filter8
: true,
736 group3__filter9
: true
738 'A \'string_options\' parameter containing all values, results in all filters of the group as checked.'
741 model
.toggleFiltersSelected(
742 model
.getFiltersFromParameters( {
743 group3
: 'filter7,all,filter9'
747 model
.getSelectedState(),
748 $.extend( {}, baseFilterRepresentation
, {
749 group3__filter7
: true,
750 group3__filter8
: true,
751 group3__filter9
: true
753 'A \'string_options\' parameter containing the value \'all\', results in all filters of the group as checked.'
756 model
.toggleFiltersSelected(
757 model
.getFiltersFromParameters( {
758 group3
: 'filter7,foo,filter9'
762 model
.getSelectedState(),
763 $.extend( {}, baseFilterRepresentation
, {
764 group3__filter7
: true,
765 group3__filter8
: false,
766 group3__filter9
: true
768 'A \'string_options\' parameter containing an invalid value, results in the invalid value ignored and the valid corresponding filters checked.'
771 model
.toggleFiltersSelected(
772 model
.getFiltersFromParameters( {
777 model
.getSelectedState(),
778 $.extend( {}, baseFilterRepresentation
, {
779 group4__option1
: true,
780 group4__option2
: false
782 'A \'single_option\' parameter reflects a single selected value.'
786 model
.getFiltersFromParameters( {
787 group4
: 'option1,option2'
789 baseFilterRepresentation
,
790 'An invalid \'single_option\' parameter is ignored.'
793 // Change to one value
794 model
.toggleFiltersSelected(
795 model
.getFiltersFromParameters( {
799 // Change again to another value
800 model
.toggleFiltersSelected(
801 model
.getFiltersFromParameters( {
806 model
.getSelectedState(),
807 $.extend( {}, baseFilterRepresentation
, {
808 group4__option2
: true
810 'A \'single_option\' parameter always reflects the latest selected value.'
814 QUnit
.test( 'sanitizeStringOptionGroup', function ( assert
) {
815 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
817 model
.initializeFilters( filterDefinition
, viewsDefinition
);
820 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'filter1', 'filter2' ] ),
821 [ 'filter1', 'filter2' ],
822 'Remove duplicate values'
826 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'foo', 'filter2' ] ),
827 [ 'filter1', 'filter2' ],
828 'Remove invalid values'
832 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'all', 'filter2' ] ),
834 'If any value is "all", the only value is "all".'
838 QUnit
.test( 'Filter interaction: subsets', function ( assert
) {
839 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
841 model
.initializeFilters( filterDefinition
, viewsDefinition
);
843 // Select a filter that has subset with another filter
844 model
.toggleFiltersSelected( {
845 group1__filter1
: true
848 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
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 }
860 'Filters with subsets are represented in the model.'
863 // Select another filter that has a subset with the same previous filter
864 model
.toggleFiltersSelected( {
865 group1__filter2
: true
867 model
.reassessFilterInteractions( model
.getItemByName( 'filter2' ) );
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 }
877 'Filters that have multiple subsets are represented.'
880 // Remove one filter (but leave the other) that affects filter3
881 model
.toggleFiltersSelected( {
882 group1__filter1
: false
884 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
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 }
893 'Removing a filter only un-includes its subset if there is no other filter affecting.'
896 model
.toggleFiltersSelected( {
897 group1__filter2
: false
899 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
901 model
.getFullState(),
903 'Removing all supersets also un-includes the subsets.'
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();
913 // This is the logic inside the capsule widget
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()
921 getCurrentItemsMutedState = function () {
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' )
932 group1__filter1
: false,
933 group1__filter2
: false,
934 group1__filter3
: false,
935 group2__filter4
: false,
936 group2__filter5
: false,
937 group2__filter6
: false
940 model
.initializeFilters( filterDefinition
, viewsDefinition
);
942 // Starting state, no selection, all items are non-muted
944 getCurrentItemsMutedState(),
946 'No selection - all items are non-muted'
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
957 // Both groups have multiple (but not all) items selected, all items are non-muted
959 getCurrentItemsMutedState(),
961 'Not all items in the group selected - all items are non-muted'
964 // Select all items in 'fullCoverage' group (group2)
965 model
.toggleFiltersSelected( {
966 group2__filter6
: true
969 // Group2 (full coverage) has all items selected, all its items are muted
971 getCurrentItemsMutedState(),
972 $.extend( {}, baseMuteState
, {
973 group2__filter4
: true,
974 group2__filter5
: true,
975 group2__filter6
: true
977 'All items in \'full coverage\' group are selected - all items in the group are muted'
980 // Select all items in non 'fullCoverage' group (group1)
981 model
.toggleFiltersSelected( {
982 group1__filter3
: true
985 // Group1 (full coverage) has all items selected, no items in it are muted (non full coverage)
987 getCurrentItemsMutedState(),
988 $.extend( {}, baseMuteState
, {
989 group2__filter4
: true,
990 group2__filter5
: true,
991 group2__filter6
: true
993 'All items in a non \'full coverage\' group are selected - none of the items in the group are muted'
996 // Uncheck an item from each group
997 model
.toggleFiltersSelected( {
998 group1__filter3
: false,
999 group2__filter5
: false
1002 getCurrentItemsMutedState(),
1004 'Not all items in the group are checked - all items are non-muted regardless of group coverage'
1008 QUnit
.test( 'Filter interaction: conflicts', function ( assert
) {
1009 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1011 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1014 model
.getFullState(),
1015 baseFullFilterState
,
1016 'Initial state: no conflicts because no selections.'
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)
1024 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
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 }
1037 'Selecting a filter that conflicts with a group sets all the conflicted group items as "conflicted".'
1040 // Select one of the conflicts (both filters are now conflicted and selected)
1041 model
.toggleFiltersSelected( {
1042 group2__filter4
: true // conflicts: filter 1
1044 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter4' ) );
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 }
1057 'Selecting a conflicting filter inside a group, sets both sides to conflicted and selected.'
1061 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1062 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1064 // Select a filter that has a conflict with a specific filter
1065 model
.toggleFiltersSelected( {
1066 group1__filter2
: true // conflicts: filter6
1068 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
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 }
1078 'Selecting a filter that conflicts with another filter sets the other as "conflicted".'
1081 // Select the conflicting filter
1082 model
.toggleFiltersSelected( {
1083 group2__filter6
: true // conflicts: filter2
1086 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter6' ) );
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 },
1100 // Subsets are affected by the selection
1101 group1__filter3
: { included
: true }
1103 'Selecting a conflicting filter with an individual filter, sets both sides to conflicted and selected.'
1106 // Now choose a non-conflicting filter from the group
1107 model
.toggleFiltersSelected( {
1108 group2__filter5
: true
1111 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter5' ) );
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
1124 // Subsets are affected by the selection
1125 group1__filter3
: { included
: true }
1127 'Selecting a non-conflicting filter within the group of a conflicting filter removes the conflicts.'
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
1138 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1141 model
.getFullState(),
1142 $.extend( true, {}, baseFullFilterState
, {
1143 group1__filter1
: { conflicted
: true },
1144 group2__filter6
: { selected
: true },
1145 group2__filter5
: { selected
: true }
1147 'Unselecting an item that did not conflict returns the conflict state.'
1150 // Followup #2: Now actually select filter1, and make everything conflicted
1151 model
.toggleFiltersSelected( {
1152 group1__filter1
: true
1155 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
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 }
1168 'Selecting an item that conflicts with a whole group makes all selections in that group conflicted.'
1173 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1174 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1176 // Select a filter that has a conflict with a specific filter
1177 model
.toggleFiltersSelected( {
1178 group1__filter2
: true // conflicts: filter6
1181 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
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 }
1191 'Simple case: Selecting a filter that conflicts with another filter sets the other as "conflicted".'
1194 model
.toggleFiltersSelected( {
1195 group1__filter3
: true // conflicts: filter6
1198 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter3' ) );
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 }
1207 'Simple case: Selecting a filter that is not in conflict removes the conflict.'
1211 QUnit
.test( 'Filter highlights', function ( assert
) {
1212 // We are using a different (smaller) definition here than the global one
1213 var definition
= [ {
1216 type
: 'string_options',
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' }
1226 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1228 model
.initializeFilters( definition
);
1231 !model
.isHighlightEnabled(),
1232 'Initially, highlight is disabled.'
1235 model
.toggleHighlight( true );
1237 model
.isHighlightEnabled(),
1238 'Highlight is enabled on toggle.'
1241 model
.setHighlightColor( 'group1__filter1', 'color1' );
1242 model
.setHighlightColor( 'group1__filter2', 'color2' );
1245 model
.getHighlightedItems().map( function ( item
) {
1246 return item
.getName();
1252 'Highlighted items are highlighted.'
1256 model
.getItemByName( 'group1__filter1' ).getHighlightColor(),
1258 'Item highlight color is set.'
1261 model
.setHighlightColor( 'group1__filter1', 'color1changed' );
1263 model
.getItemByName( 'group1__filter1' ).getHighlightColor(),
1265 'Item highlight color is changed on setHighlightColor.'
1268 model
.clearHighlightColor( 'group1__filter1' );
1270 model
.getHighlightedItems().map( function ( item
) {
1271 return item
.getName();
1276 'Clear highlight from an item results in the item no longer being highlighted.'
1280 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1281 model
.initializeFilters( definition
);
1283 model
.setHighlightColor( 'group1__filter1', 'color1' );
1284 model
.setHighlightColor( 'group1__filter2', 'color2' );
1285 model
.setHighlightColor( 'group1__filter3', 'color3' );
1288 model
.getHighlightedItems().map( function ( item
) {
1289 return item
.getName();
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.
1306 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1307 model
.initializeFilters( definition
);
1309 model
.setHighlightColor( 'group1__filter1', 'color1' );
1310 model
.setHighlightColor( 'group1__filter6', 'color6' );
1313 model
.getHighlightedItems().map( function ( item
) {
1314 return item
.getName();
1319 'Items without a specified class identifier are not highlighted.'
1322 }( mediaWiki
, jQuery
) );