1 /* eslint-disable camelcase */
3 var filterDefinition
= [ {
5 type
: 'send_unselected_if_any',
8 name
: 'filter1', label
: 'group1filter1-label', description
: 'group1filter1-desc',
10 cssClass
: 'filter1class',
11 conflicts
: [ { group
: 'group2' } ],
24 name
: 'filter2', label
: 'group1filter2-label', description
: 'group1filter2-desc',
25 conflicts
: [ { group
: 'group2', filter
: 'filter6' } ],
26 cssClass
: 'filter2class',
34 // NOTE: This filter has no highlight!
35 { name
: 'filter3', label
: 'group1filter3-label', description
: 'group1filter3-desc', default: true }
39 type
: 'send_unselected_if_any',
41 conflicts
: [ { group
: 'group1', filter
: 'filter1' } ],
43 { name
: 'filter4', label
: 'group2filter4-label', description
: 'group2filter4-desc', cssClass
: 'filter4class' },
44 { name
: 'filter5', label
: 'group2filter5-label', description
: 'group2filter5-desc', default: true, cssClass
: 'filter5class' },
46 name
: 'filter6', label
: 'group2filter6-label', description
: 'group2filter6-desc', cssClass
: 'filter6class',
47 conflicts
: [ { group
: 'group1', filter
: 'filter2' } ]
52 type
: 'string_options',
56 { name
: 'filter7', label
: 'group3filter7-label', description
: 'group3filter7-desc', cssClass
: 'filter7class' },
57 { name
: 'filter8', label
: 'group3filter8-label', description
: 'group3filter8-desc', cssClass
: 'filter8class' },
58 { name
: 'filter9', label
: 'group3filter9-label', description
: 'group3filter9-desc', cssClass
: 'filter9class' }
62 type
: 'single_option',
66 // NOTE: The entire group has no highlight supported
67 { name
: 'option1', label
: 'group4option1-label', description
: 'group4option1-desc' },
68 { name
: 'option2', label
: 'group4option2-label', description
: 'group4option2-desc' },
69 { name
: 'option3', label
: 'group4option3-label', description
: 'group4option3-desc' }
73 type
: 'single_option',
75 { name
: 'option1', label
: 'group5option1-label', description
: 'group5option1-desc', cssClass
: 'group5opt1class' },
76 { name
: 'option2', label
: 'group5option2-label', description
: 'group5option2-desc', cssClass
: 'group5opt2class' },
77 { name
: 'option3', label
: 'group5option3-label', description
: 'group5option3-desc', cssClass
: 'group5opt3class' }
84 { name
: 'group6option1', label
: 'group6option1-label', description
: 'group6option1-desc', cssClass
: 'group6opt1class' },
85 { name
: 'group6option2', label
: 'group6option2-label', description
: 'group6option2-desc', default: true, cssClass
: 'group6opt2class' },
86 { name
: 'group6option3', label
: 'group6option3-label', description
: 'group6option3-desc', default: true, cssClass
: 'group6opt3class' }
90 type
: 'single_option',
92 default: 'group7option2',
94 { name
: 'group7option1', label
: 'group7option1-label', description
: 'group7option1-desc', cssClass
: 'group7opt1class' },
95 { name
: 'group7option2', label
: 'group7option2-label', description
: 'group7option2-desc', cssClass
: 'group7opt2class' },
96 { name
: 'group7option3', label
: 'group7option3-label', description
: 'group7option3-desc', cssClass
: 'group7opt3class' }
99 shortFilterDefinition
= [ {
101 type
: 'send_unselected_if_any',
102 filters
: [ { name
: 'filter1' }, { name
: 'filter2' } ]
107 filters
: [ { name
: 'filter3' }, { name
: 'filter4' } ]
110 type
: 'string_options',
113 filters
: [ { name
: 'filter5' }, { name
: 'filter6' }, { name
: 'filter7' } ]
122 type
: 'string_options',
125 { name
: 0, label
: 'Main', cssClass
: 'namespace-0' },
126 { name
: 1, label
: 'Talk', cssClass
: 'namespace-1' },
127 { name
: 2, label
: 'User', cssClass
: 'namespace-2' },
128 { name
: 3, label
: 'User talk', cssClass
: 'namespace-3' }
133 defaultParameters
= {
145 baseParamRepresentation
= {
158 group7
: 'group7option2',
161 emptyParamRepresentation
= {
177 group1__filter1_color
: null,
178 group1__filter2_color
: null,
179 // group1__filter3_color: null, // Highlight isn't supported
180 group2__filter4_color
: null,
181 group2__filter5_color
: null,
182 group2__filter6_color
: null,
183 group3__filter7_color
: null,
184 group3__filter8_color
: null,
185 group3__filter9_color
: null,
186 // group4__option1_color: null, // Highlight isn't supported
187 // group4__option2_color: null, // Highlight isn't supported
188 // group4__option3_color: null, // Highlight isn't supported
189 group5__option1_color
: null,
190 group5__option2_color
: null,
191 group5__option3_color
: null,
192 group6__group6option1_color
: null,
193 group6__group6option2_color
: null,
194 group6__group6option3_color
: null,
195 group7__group7option1_color
: null,
196 group7__group7option2_color
: null,
197 group7__group7option3_color
: null,
198 namespace__0_color
: null,
199 namespace__1_color
: null,
200 namespace__2_color
: null,
201 namespace__3_color
: null
203 baseFilterRepresentation
= {
204 group1__filter1
: false,
205 group1__filter2
: false,
206 group1__filter3
: false,
207 group2__filter4
: false,
208 group2__filter5
: false,
209 group2__filter6
: false,
210 group3__filter7
: false,
211 group3__filter8
: false,
212 group3__filter9
: false,
213 // The 'single_value' type of group can't have empty value; it's either
214 // the default given or the first item that will get the truthy value
215 group4__option1
: false,
216 group4__option2
: true, // Default
217 group4__option3
: false,
218 group5__option1
: true, // No default set, first item is default value
219 group5__option2
: false,
220 group5__option3
: false,
221 group6__group6option1
: false,
222 group6__group6option2
: true,
223 group6__group6option3
: true,
224 group7__group7option1
: false,
225 group7__group7option2
: true,
226 group7__group7option3
: false,
232 baseFullFilterState
= {
233 group1__filter1
: { selected
: false, conflicted
: false, included
: false },
234 group1__filter2
: { selected
: false, conflicted
: false, included
: false },
235 group1__filter3
: { selected
: false, conflicted
: false, included
: false },
236 group2__filter4
: { selected
: false, conflicted
: false, included
: false },
237 group2__filter5
: { selected
: false, conflicted
: false, included
: false },
238 group2__filter6
: { selected
: false, conflicted
: false, included
: false },
239 group3__filter7
: { selected
: false, conflicted
: false, included
: false },
240 group3__filter8
: { selected
: false, conflicted
: false, included
: false },
241 group3__filter9
: { selected
: false, conflicted
: false, included
: false },
242 group4__option1
: { selected
: false, conflicted
: false, included
: false },
243 group4__option2
: { selected
: true, conflicted
: false, included
: false },
244 group4__option3
: { selected
: false, conflicted
: false, included
: false },
245 group5__option1
: { selected
: true, conflicted
: false, included
: false },
246 group5__option2
: { selected
: false, conflicted
: false, included
: false },
247 group5__option3
: { selected
: false, conflicted
: false, included
: false },
248 group6__group6option1
: { selected
: false, conflicted
: false, included
: false },
249 group6__group6option2
: { selected
: true, conflicted
: false, included
: false },
250 group6__group6option3
: { selected
: true, conflicted
: false, included
: false },
251 group7__group7option1
: { selected
: false, conflicted
: false, included
: false },
252 group7__group7option2
: { selected
: true, conflicted
: false, included
: false },
253 group7__group7option3
: { selected
: false, conflicted
: false, included
: false },
254 namespace__0
: { selected
: false, conflicted
: false, included
: false },
255 namespace__1
: { selected
: false, conflicted
: false, included
: false },
256 namespace__2
: { selected
: false, conflicted
: false, included
: false },
257 namespace__3
: { selected
: false, conflicted
: false, included
: false }
260 QUnit
.module( 'mediawiki.rcfilters - FiltersViewModel', QUnit
.newMwEnvironment( {
262 'group1filter1-label': 'Group 1: Filter 1 title',
263 'group1filter1-desc': 'Description of Filter 1 in Group 1',
264 'group1filter2-label': 'Group 1: Filter 2 title',
265 'group1filter2-desc': 'Description of Filter 2 in Group 1',
266 'group1filter3-label': 'Group 1: Filter 3',
267 'group1filter3-desc': 'Description of Filter 3 in Group 1',
269 'group2filter4-label': 'Group 2: Filter 4 title',
270 'group2filter4-desc': 'Description of Filter 4 in Group 2',
271 'group2filter5-label': 'Group 2: Filter 5',
272 'group2filter5-desc': 'Description of Filter 5 in Group 2',
273 'group2filter6-label': 'xGroup 2: Filter 6',
274 'group2filter6-desc': 'Description of Filter 6 in Group 2'
278 QUnit
.test( 'Setting up filters', function ( assert
) {
279 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
281 model
.initializeFilters( filterDefinition
, viewsDefinition
);
283 // Test that all items were created
285 Object
.keys( baseFilterRepresentation
).every( function ( filterName
) {
286 return model
.getItemByName( filterName
) instanceof mw
.rcfilters
.dm
.FilterItem
;
288 'Filters instantiated and stored correctly'
292 model
.getSelectedState(),
293 baseFilterRepresentation
,
294 'Initial state of filters'
297 model
.toggleFiltersSelected( {
298 group1__filter1
: true,
299 group2__filter5
: true,
300 group3__filter7
: true
303 model
.getSelectedState(),
304 $.extend( true, {}, baseFilterRepresentation
, {
305 group1__filter1
: true,
306 group2__filter5
: true,
307 group3__filter7
: true
309 'Updating filter states correctly'
313 QUnit
.test( 'Default filters', function ( assert
) {
314 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
316 model
.initializeFilters( filterDefinition
, viewsDefinition
);
318 // Empty query = only default values
320 model
.getDefaultParams(),
322 'Default parameters are stored properly per filter and group (sticky groups are ignored)'
326 QUnit
.test( 'Parameter minimal state', function ( assert
) {
327 var model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
332 msg
: 'Empty parameter representation produces an empty result'
346 msg
: 'Mixed input results in only non-falsey values as result'
355 group1__filter1_color
: null
358 msg
: 'An all-falsey input results in an empty result.'
367 group1__filter1_color
: 'c1'
370 group1__filter1_color
: 'c1'
372 msg
: 'An all-falsey input with highlight params result in only the highlight param.'
376 group1__filter1_color
: 'c1',
377 group1__filter3_color
: 'c3' // Not supporting highlights
380 group1__filter1_color
: 'c1'
382 msg
: 'Unsupported highlights are removed.'
386 model
.initializeFilters( filterDefinition
, viewsDefinition
);
388 cases
.forEach( function ( test
) {
390 model
.getMinimizedParamRepresentation( test
.input
),
397 QUnit
.test( 'Parameter states', function ( assert
) {
398 // Some groups / params have their defaults immediately applied
399 // to their state. These include single_option which can never
400 // be empty, etc. These are these states:
401 var parametersWithoutExcluded
,
402 appliedDefaultParameters
= {
405 // Sticky, their defaults apply immediately
408 group7
: 'group7option2'
410 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
412 model
.initializeFilters( filterDefinition
, viewsDefinition
);
414 model
.getEmptyParameterState(),
415 emptyParamRepresentation
,
416 'Producing an empty parameter state'
419 model
.toggleFiltersSelected( {
420 group1__filter1
: true,
421 group3__filter7
: true
425 model
.getCurrentParameterState(),
426 // appliedDefaultParams applies the default value to parameters
427 // who must have an initial value to begin with, so we have to
428 // take it into account in the current state
429 $.extend( true, {}, appliedDefaultParameters
, {
434 'Producing a current parameter state'
438 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
439 model
.initializeFilters( filterDefinition
, viewsDefinition
);
441 parametersWithoutExcluded
= $.extend( true, {}, appliedDefaultParameters
);
442 delete parametersWithoutExcluded
.group7
;
443 delete parametersWithoutExcluded
.group6option2
;
444 delete parametersWithoutExcluded
.group6option3
;
447 model
.getCurrentParameterState( true ),
448 parametersWithoutExcluded
,
449 'Producing a current clean parameter state without excluded filters'
453 QUnit
.test( 'Cleaning up parameter states', function ( assert
) {
454 var model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
459 msg
: 'Empty parameter representation produces an empty result'
463 filter1
: '1', // Regular (do not strip)
464 group6option1
: '1' // Sticky
466 result
: { filter1
: '1' },
467 msg
: 'Valid input strips all sticky params regardless of value'
471 model
.initializeFilters( filterDefinition
, viewsDefinition
);
473 cases
.forEach( function ( test
) {
475 model
.removeStickyParams( test
.input
),
483 QUnit
.test( 'Finding matching filters', function ( assert
) {
489 group1
: [ 'group1__filter1', 'group1__filter2', 'group1__filter3' ],
490 group2
: [ 'group2__filter4', 'group2__filter5' ]
492 reason
: 'Finds filters starting with the query string'
497 group2
: [ 'group2__filter4', 'group2__filter5', 'group2__filter6' ]
499 reason
: 'Finds filters containing the query string in their description'
504 group1
: [ 'group1__filter1', 'group1__filter2' ],
505 group2
: [ 'group2__filter4' ]
507 reason
: 'Finds filters containing the query string in their group title'
512 namespace: [ 'namespace__0' ]
514 reason
: 'Finds item in view when a prefix is used'
519 reason
: 'Finds no results if using namespaces prefix (:) to search for filter title'
522 model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
523 extractNames = function ( matches
) {
525 Object
.keys( matches
).forEach( function ( groupName
) {
526 result
[ groupName
] = matches
[ groupName
].map( function ( item
) {
527 return item
.getName();
533 model
.initializeFilters( filterDefinition
, viewsDefinition
);
535 testCases
.forEach( function ( testCase
) {
536 matches
= model
.findMatches( testCase
.query
);
538 extractNames( matches
),
539 testCase
.expectedMatches
,
544 matches
= model
.findMatches( 'foo' );
546 $.isEmptyObject( matches
),
547 'findMatches returns an empty object when no results found'
551 QUnit
.test( 'getParametersFromFilters', function ( assert
) {
552 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
554 model
.initializeFilters( filterDefinition
, viewsDefinition
);
556 // Starting with all filters unselected
558 model
.getParametersFromFilters(),
559 baseParamRepresentation
,
560 'Unselected filters return all parameters falsey or \'\'.'
564 model
.toggleFiltersSelected( {
565 group1__filter1
: true
567 // Only one filter in one group
569 model
.getParametersFromFilters(),
570 $.extend( true, {}, baseParamRepresentation
, {
571 // Group 1 (one selected, the others are true)
575 'One filter in one "send_unselected_if_any" group returns the other parameters truthy.'
579 model
.toggleFiltersSelected( {
580 group1__filter1
: true,
581 group1__filter2
: true
583 // Two selected filters in one group
585 model
.getParametersFromFilters(),
586 $.extend( true, {}, baseParamRepresentation
, {
587 // Group 1 (two selected, the other is true)
590 'Two filters in one "send_unselected_if_any" group returns the other parameters truthy.'
594 model
.toggleFiltersSelected( {
595 group1__filter1
: true,
596 group1__filter2
: true,
597 group1__filter3
: true
599 // All filters of the group are selected == this is the same as not selecting any
601 model
.getParametersFromFilters(),
602 baseParamRepresentation
,
603 'All filters selected in one "send_unselected_if_any" group returns all parameters falsy.'
606 // Select 1 filter from string_options
607 model
.toggleFiltersSelected( {
608 group3__filter7
: true,
609 group3__filter8
: false,
610 group3__filter9
: false
612 // All filters of the group are selected == this is the same as not selecting any
614 model
.getParametersFromFilters(),
615 $.extend( true, {}, baseParamRepresentation
, {
618 'One filter selected in "string_option" group returns that filter in the value.'
621 // Select 2 filters from string_options
622 model
.toggleFiltersSelected( {
623 group3__filter7
: true,
624 group3__filter8
: true,
625 group3__filter9
: false
627 // All filters of the group are selected == this is the same as not selecting any
629 model
.getParametersFromFilters(),
630 $.extend( true, {}, baseParamRepresentation
, {
631 group3
: 'filter7,filter8'
633 'Two filters selected in "string_option" group returns those filters in the value.'
636 // Select 3 filters from string_options
637 model
.toggleFiltersSelected( {
638 group3__filter7
: true,
639 group3__filter8
: true,
640 group3__filter9
: true
642 // All filters of the group are selected == this is the same as not selecting any
644 model
.getParametersFromFilters(),
645 $.extend( true, {}, baseParamRepresentation
, {
648 'All filters selected in "string_option" group returns \'all\'.'
652 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
653 model
.initializeFilters( filterDefinition
, viewsDefinition
);
655 // Select an option from single_option group
656 model
.toggleFiltersSelected( {
657 group4__option2
: true
659 // All filters of the group are selected == this is the same as not selecting any
661 model
.getParametersFromFilters(),
662 $.extend( true, {}, baseParamRepresentation
, {
665 'Selecting an option from "single_option" group returns that option as a value.'
668 // Select a different option from single_option group
669 model
.toggleFiltersSelected( {
670 group4__option3
: true
672 // All filters of the group are selected == this is the same as not selecting any
674 model
.getParametersFromFilters(),
675 $.extend( true, {}, baseParamRepresentation
, {
678 'Selecting a different option from "single_option" group changes the selection.'
682 QUnit
.test( 'getParametersFromFilters (custom object)', function ( assert
) {
683 // This entire test uses different base definition than the global one
684 // on purpose, to verify that the values inserted as a custom object
685 // are the ones we expect in return
687 model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
691 type
: 'send_unselected_if_any',
693 { name
: 'hidefilter1', label
: 'Hide filter 1', description
: '' },
694 { name
: 'hidefilter2', label
: 'Hide filter 2', description
: '' },
695 { name
: 'hidefilter3', label
: 'Hide filter 3', description
: '' }
700 type
: 'send_unselected_if_any',
702 { name
: 'hidefilter4', label
: 'Hide filter 4', description
: '' },
703 { name
: 'hidefilter5', label
: 'Hide filter 5', description
: '' },
704 { name
: 'hidefilter6', label
: 'Hide filter 6', description
: '' }
709 type
: 'string_options',
712 { name
: 'filter7', label
: 'Hide filter 7', description
: '' },
713 { name
: 'filter8', label
: 'Hide filter 8', description
: '' },
714 { name
: 'filter9', label
: 'Hide filter 9', description
: '' }
719 type
: 'single_option',
721 { name
: 'filter10', label
: 'Hide filter 10', description
: '' },
722 { name
: 'filter11', label
: 'Hide filter 11', description
: '' },
723 { name
: 'filter12', label
: 'Hide filter 12', description
: '' }
738 // This is mocking the cases above, both
739 // - 'Two filters in one "send_unselected_if_any" group returns the other parameters truthy.'
740 // - 'Two filters selected in "string_option" group returns those filters in the value.'
742 group1__hidefilter1
: true,
743 group1__hidefilter2
: true,
744 group1__hidefilter3
: false,
745 group2__hidefilter4
: false,
746 group2__hidefilter5
: false,
747 group2__hidefilter6
: false,
748 group3__filter7
: true,
749 group3__filter8
: true,
750 group3__filter9
: false
752 expected
: $.extend( true, {}, baseResult
, {
753 // Group 1 (two selected, the others are true)
755 // Group 3 (two selected)
756 group3
: 'filter7,filter8'
758 msg
: 'Given an explicit (complete) filter state object, the result is the same as if the object given represented the model state.'
761 // This is mocking case above
762 // - 'One filter in one "send_unselected_if_any" group returns the other parameters truthy.'
764 group1__hidefilter1
: 1
766 expected
: $.extend( true, {}, baseResult
, {
767 // Group 1 (one selected, the others are true)
771 msg
: 'Given an explicit (incomplete) filter state object, the result is the same as if the object give represented the model state.'
775 group4__filter10
: true
777 expected
: $.extend( true, {}, baseResult
, {
780 msg
: 'Given a single value for "single_option" that option is represented in the result.'
784 group4__filter10
: true,
785 group4__filter11
: true
787 expected
: $.extend( true, {}, baseResult
, {
790 msg
: 'Given more than one true value for "single_option" (which should not happen!) only the first value counts, and the second is ignored.'
794 expected
: baseResult
,
795 msg
: 'Given an explicit empty object, the result is all filters set to their falsey unselected value.'
799 model
.initializeFilters( definition
);
800 // Store original state
801 originalState
= model
.getSelectedState();
804 cases
.forEach( function ( test
) {
806 model
.getParametersFromFilters( test
.input
),
812 // After doing the above tests, make sure the actual state
813 // of the filter stayed the same
815 model
.getSelectedState(),
817 'Running the method with external definition to parse does not actually change the state of the model'
821 QUnit
.test( 'getFiltersFromParameters', function ( assert
) {
822 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
824 model
.initializeFilters( filterDefinition
, viewsDefinition
);
826 // Empty query = only default values
828 model
.getFiltersFromParameters( {} ),
829 baseFilterRepresentation
,
830 'Empty parameter query results in an object representing all filters set to their base state'
834 model
.getFiltersFromParameters( {
837 $.extend( {}, baseFilterRepresentation
, {
838 group1__filter1
: true, // The text is "show filter 1"
839 group1__filter2
: false, // The text is "show filter 2"
840 group1__filter3
: true // The text is "show filter 3"
842 'One truthy parameter in a group whose other parameters are true by default makes the rest of the filters in the group false (unchecked)'
846 model
.getFiltersFromParameters( {
851 $.extend( {}, baseFilterRepresentation
, {
852 group1__filter1
: false, // The text is "show filter 1"
853 group1__filter2
: false, // The text is "show filter 2"
854 group1__filter3
: false // The text is "show filter 3"
856 'All paremeters in the same \'send_unselected_if_any\' group false is equivalent to none are truthy (checked) in the interface'
859 // The ones above don't update the model, so we have a clean state.
860 // getFiltersFromParameters is stateless; any change is unaffected by the current state
861 // This test is demonstrating wrong usage of the method;
862 // We should be aware that getFiltersFromParameters is stateless,
863 // so each call gives us a filter state that only reflects the query given.
864 // This means that the two calls to toggleFiltersSelected() below collide.
865 // The result of the first is overridden by the result of the second,
866 // since both get a full state object from getFiltersFromParameters that **only** relates
867 // to the input it receives.
868 model
.toggleFiltersSelected(
869 model
.getFiltersFromParameters( {
874 model
.toggleFiltersSelected(
875 model
.getFiltersFromParameters( {
880 // The result here is ignoring the first toggleFiltersSelected call
882 model
.getSelectedState(),
883 $.extend( {}, baseFilterRepresentation
, {
884 group2__filter4
: true,
885 group2__filter5
: true,
886 group2__filter6
: false
888 'getFiltersFromParameters does not care about previous or existing state.'
892 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
893 model
.initializeFilters( filterDefinition
, viewsDefinition
);
895 model
.toggleFiltersSelected(
896 model
.getFiltersFromParameters( {
901 model
.getSelectedState(),
902 $.extend( {}, baseFilterRepresentation
, {
903 group3__filter7
: true,
904 group3__filter8
: false,
905 group3__filter9
: false
907 'A \'string_options\' parameter containing 1 value, results in the corresponding filter as checked'
910 model
.toggleFiltersSelected(
911 model
.getFiltersFromParameters( {
912 group3
: 'filter7,filter8'
916 model
.getSelectedState(),
917 $.extend( {}, baseFilterRepresentation
, {
918 group3__filter7
: true,
919 group3__filter8
: true,
920 group3__filter9
: false
922 'A \'string_options\' parameter containing 2 values, results in both corresponding filters as checked'
925 model
.toggleFiltersSelected(
926 model
.getFiltersFromParameters( {
927 group3
: 'filter7,filter8,filter9'
931 model
.getSelectedState(),
932 $.extend( {}, baseFilterRepresentation
, {
933 group3__filter7
: true,
934 group3__filter8
: true,
935 group3__filter9
: true
937 'A \'string_options\' parameter containing all values, results in all filters of the group as checked.'
940 model
.toggleFiltersSelected(
941 model
.getFiltersFromParameters( {
942 group3
: 'filter7,all,filter9'
946 model
.getSelectedState(),
947 $.extend( {}, baseFilterRepresentation
, {
948 group3__filter7
: true,
949 group3__filter8
: true,
950 group3__filter9
: true
952 'A \'string_options\' parameter containing the value \'all\', results in all filters of the group as checked.'
955 model
.toggleFiltersSelected(
956 model
.getFiltersFromParameters( {
957 group3
: 'filter7,foo,filter9'
961 model
.getSelectedState(),
962 $.extend( {}, baseFilterRepresentation
, {
963 group3__filter7
: true,
964 group3__filter8
: false,
965 group3__filter9
: true
967 'A \'string_options\' parameter containing an invalid value, results in the invalid value ignored and the valid corresponding filters checked.'
970 model
.toggleFiltersSelected(
971 model
.getFiltersFromParameters( {
976 model
.getSelectedState(),
977 $.extend( {}, baseFilterRepresentation
, {
978 group4__option1
: true,
979 group4__option2
: false
981 'A \'single_option\' parameter reflects a single selected value.'
985 model
.getFiltersFromParameters( {
986 group4
: 'option1,option2'
988 baseFilterRepresentation
,
989 'An invalid \'single_option\' parameter is ignored.'
992 // Change to one value
993 model
.toggleFiltersSelected(
994 model
.getFiltersFromParameters( {
998 // Change again to another value
999 model
.toggleFiltersSelected(
1000 model
.getFiltersFromParameters( {
1005 model
.getSelectedState(),
1006 $.extend( {}, baseFilterRepresentation
, {
1007 group4__option2
: true
1009 'A \'single_option\' parameter always reflects the latest selected value.'
1013 QUnit
.test( 'sanitizeStringOptionGroup', function ( assert
) {
1014 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1016 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1019 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'filter1', 'filter2' ] ),
1020 [ 'filter1', 'filter2' ],
1021 'Remove duplicate values'
1025 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'foo', 'filter2' ] ),
1026 [ 'filter1', 'filter2' ],
1027 'Remove invalid values'
1031 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'all', 'filter2' ] ),
1033 'If any value is "all", the only value is "all".'
1037 QUnit
.test( 'Filter interaction: subsets', function ( assert
) {
1038 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1040 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1042 // Select a filter that has subset with another filter
1043 model
.toggleFiltersSelected( {
1044 group1__filter1
: true
1047 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
1049 model
.getFullState(),
1050 $.extend( true, {}, baseFullFilterState
, {
1051 group1__filter1
: { selected
: true },
1052 group1__filter2
: { included
: true },
1053 group1__filter3
: { included
: true },
1054 // Conflicts are affected
1055 group2__filter4
: { conflicted
: true },
1056 group2__filter5
: { conflicted
: true },
1057 group2__filter6
: { conflicted
: true }
1059 'Filters with subsets are represented in the model.'
1062 // Select another filter that has a subset with the same previous filter
1063 model
.toggleFiltersSelected( {
1064 group1__filter2
: true
1066 model
.reassessFilterInteractions( model
.getItemByName( 'filter2' ) );
1068 model
.getFullState(),
1069 $.extend( true, {}, baseFullFilterState
, {
1070 group1__filter1
: { selected
: true },
1071 group1__filter2
: { selected
: true, included
: true },
1072 group1__filter3
: { included
: true },
1073 // Conflicts are affected
1074 group2__filter6
: { conflicted
: true }
1076 'Filters that have multiple subsets are represented.'
1079 // Remove one filter (but leave the other) that affects filter3
1080 model
.toggleFiltersSelected( {
1081 group1__filter1
: false
1083 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
1085 model
.getFullState(),
1086 $.extend( true, {}, baseFullFilterState
, {
1087 group1__filter2
: { selected
: true, included
: false },
1088 group1__filter3
: { included
: true },
1089 // Conflicts are affected
1090 group2__filter6
: { conflicted
: true }
1092 'Removing a filter only un-includes its subset if there is no other filter affecting.'
1095 model
.toggleFiltersSelected( {
1096 group1__filter2
: false
1098 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1100 model
.getFullState(),
1101 baseFullFilterState
,
1102 'Removing all supersets also un-includes the subsets.'
1106 QUnit
.test( 'Filter interaction: full coverage', function ( assert
) {
1107 var model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
1108 isCapsuleItemMuted = function ( filterName
) {
1109 var itemModel
= model
.getItemByName( filterName
),
1110 groupModel
= itemModel
.getGroupModel();
1112 // This is the logic inside the capsule widget
1114 // The capsule item widget only appears if the item is selected
1115 itemModel
.isSelected() &&
1116 // Muted state is only valid if group is full coverage and all items are selected
1117 groupModel
.isFullCoverage() && groupModel
.areAllSelected()
1120 getCurrentItemsMutedState = function () {
1122 group1__filter1
: isCapsuleItemMuted( 'group1__filter1' ),
1123 group1__filter2
: isCapsuleItemMuted( 'group1__filter2' ),
1124 group1__filter3
: isCapsuleItemMuted( 'group1__filter3' ),
1125 group2__filter4
: isCapsuleItemMuted( 'group2__filter4' ),
1126 group2__filter5
: isCapsuleItemMuted( 'group2__filter5' ),
1127 group2__filter6
: isCapsuleItemMuted( 'group2__filter6' )
1131 group1__filter1
: false,
1132 group1__filter2
: false,
1133 group1__filter3
: false,
1134 group2__filter4
: false,
1135 group2__filter5
: false,
1136 group2__filter6
: false
1139 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1141 // Starting state, no selection, all items are non-muted
1143 getCurrentItemsMutedState(),
1145 'No selection - all items are non-muted'
1148 // Select most (but not all) items in each group
1149 model
.toggleFiltersSelected( {
1150 group1__filter1
: true,
1151 group1__filter2
: true,
1152 group2__filter4
: true,
1153 group2__filter5
: true
1156 // Both groups have multiple (but not all) items selected, all items are non-muted
1158 getCurrentItemsMutedState(),
1160 'Not all items in the group selected - all items are non-muted'
1163 // Select all items in 'fullCoverage' group (group2)
1164 model
.toggleFiltersSelected( {
1165 group2__filter6
: true
1168 // Group2 (full coverage) has all items selected, all its items are muted
1170 getCurrentItemsMutedState(),
1171 $.extend( {}, baseMuteState
, {
1172 group2__filter4
: true,
1173 group2__filter5
: true,
1174 group2__filter6
: true
1176 'All items in \'full coverage\' group are selected - all items in the group are muted'
1179 // Select all items in non 'fullCoverage' group (group1)
1180 model
.toggleFiltersSelected( {
1181 group1__filter3
: true
1184 // Group1 (full coverage) has all items selected, no items in it are muted (non full coverage)
1186 getCurrentItemsMutedState(),
1187 $.extend( {}, baseMuteState
, {
1188 group2__filter4
: true,
1189 group2__filter5
: true,
1190 group2__filter6
: true
1192 'All items in a non \'full coverage\' group are selected - none of the items in the group are muted'
1195 // Uncheck an item from each group
1196 model
.toggleFiltersSelected( {
1197 group1__filter3
: false,
1198 group2__filter5
: false
1201 getCurrentItemsMutedState(),
1203 'Not all items in the group are checked - all items are non-muted regardless of group coverage'
1207 QUnit
.test( 'Filter interaction: conflicts', function ( assert
) {
1208 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1210 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1213 model
.getFullState(),
1214 baseFullFilterState
,
1215 'Initial state: no conflicts because no selections.'
1218 // Select a filter that has a conflict with an entire group
1219 model
.toggleFiltersSelected( {
1220 group1__filter1
: true // conflicts: entire of group 2 ( filter4, filter5, filter6)
1223 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
1226 model
.getFullState(),
1227 $.extend( true, {}, baseFullFilterState
, {
1228 group1__filter1
: { selected
: true },
1229 group2__filter4
: { conflicted
: true },
1230 group2__filter5
: { conflicted
: true },
1231 group2__filter6
: { conflicted
: true },
1232 // Subsets are affected by the selection
1233 group1__filter2
: { included
: true },
1234 group1__filter3
: { included
: true }
1236 'Selecting a filter that conflicts with a group sets all the conflicted group items as "conflicted".'
1239 // Select one of the conflicts (both filters are now conflicted and selected)
1240 model
.toggleFiltersSelected( {
1241 group2__filter4
: true // conflicts: filter 1
1243 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter4' ) );
1246 model
.getFullState(),
1247 $.extend( true, {}, baseFullFilterState
, {
1248 group1__filter1
: { selected
: true, conflicted
: true },
1249 group2__filter4
: { selected
: true, conflicted
: true },
1250 group2__filter5
: { conflicted
: true },
1251 group2__filter6
: { conflicted
: true },
1252 // Subsets are affected by the selection
1253 group1__filter2
: { included
: true },
1254 group1__filter3
: { included
: true }
1256 'Selecting a conflicting filter inside a group, sets both sides to conflicted and selected.'
1260 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1261 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1263 // Select a filter that has a conflict with a specific filter
1264 model
.toggleFiltersSelected( {
1265 group1__filter2
: true // conflicts: filter6
1267 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1270 model
.getFullState(),
1271 $.extend( true, {}, baseFullFilterState
, {
1272 group1__filter2
: { selected
: true },
1273 group2__filter6
: { conflicted
: true },
1274 // Subsets are affected by the selection
1275 group1__filter3
: { included
: true }
1277 'Selecting a filter that conflicts with another filter sets the other as "conflicted".'
1280 // Select the conflicting filter
1281 model
.toggleFiltersSelected( {
1282 group2__filter6
: true // conflicts: filter2
1285 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter6' ) );
1288 model
.getFullState(),
1289 $.extend( true, {}, baseFullFilterState
, {
1290 group1__filter2
: { selected
: true, conflicted
: true },
1291 group2__filter6
: { selected
: true, conflicted
: true },
1292 // This is added to the conflicts because filter6 is part of group2,
1293 // who is in conflict with filter1; note that filter2 also conflicts
1294 // with filter6 which means that filter1 conflicts with filter6 (because it's in group2)
1295 // and also because its **own sibling** (filter2) is **also** in conflict with the
1296 // selected items in group2 (filter6)
1297 group1__filter1
: { conflicted
: true },
1299 // Subsets are affected by the selection
1300 group1__filter3
: { included
: true }
1302 'Selecting a conflicting filter with an individual filter, sets both sides to conflicted and selected.'
1305 // Now choose a non-conflicting filter from the group
1306 model
.toggleFiltersSelected( {
1307 group2__filter5
: true
1310 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter5' ) );
1313 model
.getFullState(),
1314 $.extend( true, {}, baseFullFilterState
, {
1315 group1__filter2
: { selected
: true },
1316 group2__filter6
: { selected
: true },
1317 group2__filter5
: { selected
: true },
1318 // Filter6 and filter1 are no longer in conflict because
1319 // filter5, while it is in conflict with filter1, it is
1320 // not in conflict with filter2 - and since filter2 is
1321 // selected, it removes the conflict bidirectionally
1323 // Subsets are affected by the selection
1324 group1__filter3
: { included
: true }
1326 'Selecting a non-conflicting filter within the group of a conflicting filter removes the conflicts.'
1329 // Followup on the previous test, unselect filter2 so filter1
1330 // is now the only one selected in its own group, and since
1331 // it is in conflict with the entire of group2, it means
1332 // filter1 is once again conflicted
1333 model
.toggleFiltersSelected( {
1334 group1__filter2
: false
1337 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1340 model
.getFullState(),
1341 $.extend( true, {}, baseFullFilterState
, {
1342 group1__filter1
: { conflicted
: true },
1343 group2__filter6
: { selected
: true },
1344 group2__filter5
: { selected
: true }
1346 'Unselecting an item that did not conflict returns the conflict state.'
1349 // Followup #2: Now actually select filter1, and make everything conflicted
1350 model
.toggleFiltersSelected( {
1351 group1__filter1
: true
1354 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
1357 model
.getFullState(),
1358 $.extend( true, {}, baseFullFilterState
, {
1359 group1__filter1
: { selected
: true, conflicted
: true },
1360 group2__filter6
: { selected
: true, conflicted
: true },
1361 group2__filter5
: { selected
: true, conflicted
: true },
1362 group2__filter4
: { conflicted
: true }, // Not selected but conflicted because it's in group2
1363 // Subsets are affected by the selection
1364 group1__filter2
: { included
: true },
1365 group1__filter3
: { included
: true }
1367 'Selecting an item that conflicts with a whole group makes all selections in that group conflicted.'
1372 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1373 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1375 // Select a filter that has a conflict with a specific filter
1376 model
.toggleFiltersSelected( {
1377 group1__filter2
: true // conflicts: filter6
1380 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1383 model
.getFullState(),
1384 $.extend( true, {}, baseFullFilterState
, {
1385 group1__filter2
: { selected
: true },
1386 group2__filter6
: { conflicted
: true },
1387 // Subsets are affected by the selection
1388 group1__filter3
: { included
: true }
1390 'Simple case: Selecting a filter that conflicts with another filter sets the other as "conflicted".'
1393 model
.toggleFiltersSelected( {
1394 group1__filter3
: true // conflicts: filter6
1397 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter3' ) );
1400 model
.getFullState(),
1401 $.extend( true, {}, baseFullFilterState
, {
1402 group1__filter2
: { selected
: true },
1403 // Subsets are affected by the selection
1404 group1__filter3
: { selected
: true, included
: true }
1406 'Simple case: Selecting a filter that is not in conflict removes the conflict.'
1410 QUnit
.test( 'Filter highlights', function ( assert
) {
1411 // We are using a different (smaller) definition here than the global one
1412 var definition
= [ {
1415 type
: 'string_options',
1417 { name
: 'filter1', cssClass
: 'class1', label
: '1', description
: '1' },
1418 { name
: 'filter2', cssClass
: 'class2', label
: '2', description
: '2' },
1419 { name
: 'filter3', cssClass
: 'class3', label
: '3', description
: '3' },
1420 { name
: 'filter4', cssClass
: 'class4', label
: '4', description
: '4' },
1421 { name
: 'filter5', cssClass
: 'class5', label
: '5', description
: '5' },
1422 { name
: 'filter6', label
: '6', description
: '6' }
1425 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1427 model
.initializeFilters( definition
);
1430 model
.isHighlightEnabled(),
1432 'Initially, highlight is disabled.'
1435 model
.toggleHighlight( true );
1437 model
.isHighlightEnabled(),
1439 'Highlight is enabled on toggle.'
1442 model
.setHighlightColor( 'group1__filter1', 'color1' );
1443 model
.setHighlightColor( 'group1__filter2', 'color2' );
1446 model
.getHighlightedItems().map( function ( item
) {
1447 return item
.getName();
1453 'Highlighted items are highlighted.'
1457 model
.getItemByName( 'group1__filter1' ).getHighlightColor(),
1459 'Item highlight color is set.'
1462 model
.setHighlightColor( 'group1__filter1', 'color1changed' );
1464 model
.getItemByName( 'group1__filter1' ).getHighlightColor(),
1466 'Item highlight color is changed on setHighlightColor.'
1469 model
.clearHighlightColor( 'group1__filter1' );
1471 model
.getHighlightedItems().map( function ( item
) {
1472 return item
.getName();
1477 'Clear highlight from an item results in the item no longer being highlighted.'
1481 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1482 model
.initializeFilters( definition
);
1484 model
.setHighlightColor( 'group1__filter1', 'color1' );
1485 model
.setHighlightColor( 'group1__filter2', 'color2' );
1486 model
.setHighlightColor( 'group1__filter3', 'color3' );
1489 model
.getHighlightedItems().map( function ( item
) {
1490 return item
.getName();
1497 'Even if highlights are not enabled, the items remember their highlight state'
1498 // NOTE: When actually displaying the highlights, the UI checks whether
1499 // highlighting is generally active and then goes over the highlighted
1500 // items. The item models, however, and the view model in general, still
1501 // retains the knowledge about which filters have different colors, so we
1502 // can seamlessly return to the colors the user previously chose if they
1503 // reapply highlights.
1507 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1508 model
.initializeFilters( definition
);
1510 model
.setHighlightColor( 'group1__filter1', 'color1' );
1511 model
.setHighlightColor( 'group1__filter6', 'color6' );
1514 model
.getHighlightedItems().map( function ( item
) {
1515 return item
.getName();
1520 'Items without a specified class identifier are not highlighted.'
1524 QUnit
.test( 'emptyAllFilters', function ( assert
) {
1525 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1527 model
.initializeFilters( shortFilterDefinition
, null );
1529 model
.toggleFiltersSelected( {
1530 group1__filter1
: true,
1531 group2__filter4
: true, // hidden
1532 group3__filter5
: true // sticky
1535 model
.emptyAllFilters();
1538 model
.getSelectedState( true ),
1540 group3__filter5
: true,
1541 group3__filter6
: true
1543 'Emptying filters does not affect sticky filters'
1547 QUnit
.test( 'areVisibleFiltersEmpty', function ( assert
) {
1548 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1549 model
.initializeFilters( shortFilterDefinition
, null );
1551 model
.emptyAllFilters();
1552 assert
.strictEqual( model
.areVisibleFiltersEmpty(), true );
1554 model
.toggleFiltersSelected( {
1555 group3__filter5
: true // sticky
1557 assert
.strictEqual( model
.areVisibleFiltersEmpty(), true );
1559 model
.toggleFiltersSelected( {
1560 group1__filter1
: true
1562 assert
.strictEqual( model
.areVisibleFiltersEmpty(), false );
1564 }( mediaWiki
, jQuery
) );