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 excludedFromSavedQueries
: true,
42 conflicts
: [ { group
: 'group1', filter
: 'filter1' } ],
44 { name
: 'filter4', label
: 'group2filter4-label', description
: 'group2filter4-desc', cssClass
: 'filter4class' },
45 { name
: 'filter5', label
: 'group2filter5-label', description
: 'group2filter5-desc', default: true, cssClass
: 'filter5class' },
47 name
: 'filter6', label
: 'group2filter6-label', description
: 'group2filter6-desc', cssClass
: 'filter6class',
48 conflicts
: [ { group
: 'group1', filter
: 'filter2' } ]
53 type
: 'string_options',
57 { name
: 'filter7', label
: 'group3filter7-label', description
: 'group3filter7-desc', cssClass
: 'filter7class' },
58 { name
: 'filter8', label
: 'group3filter8-label', description
: 'group3filter8-desc', cssClass
: 'filter8class' },
59 { name
: 'filter9', label
: 'group3filter9-label', description
: 'group3filter9-desc', cssClass
: 'filter9class' }
63 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' }
106 type
: 'string_options',
109 { name
: 0, label
: 'Main', cssClass
: 'namespace-0' },
110 { name
: 1, label
: 'Talk', cssClass
: 'namespace-1' },
111 { name
: 2, label
: 'User', cssClass
: 'namespace-2' },
112 { name
: 3, label
: 'User talk', cssClass
: 'namespace-3' }
117 defaultParameters
= {
130 group7
: 'group7option2',
133 baseParamRepresentation
= {
146 group7
: 'group7option2',
149 emptyParamRepresentation
= {
166 group1__filter1_color
: null,
167 group1__filter2_color
: null,
168 // group1__filter3_color: null, // Highlight isn't supported
169 group2__filter4_color
: null,
170 group2__filter5_color
: null,
171 group2__filter6_color
: null,
172 group3__filter7_color
: null,
173 group3__filter8_color
: null,
174 group3__filter9_color
: null,
175 // group4__option1_color: null, // Highlight isn't supported
176 // group4__option2_color: null, // Highlight isn't supported
177 // group4__option3_color: null, // Highlight isn't supported
178 group5__option1_color
: null,
179 group5__option2_color
: null,
180 group5__option3_color
: null,
181 group6__group6option1_color
: null,
182 group6__group6option2_color
: null,
183 group6__group6option3_color
: null,
184 group7__group7option1_color
: null,
185 group7__group7option2_color
: null,
186 group7__group7option3_color
: null,
187 namespace__0_color
: null,
188 namespace__1_color
: null,
189 namespace__2_color
: null,
190 namespace__3_color
: null
192 baseFilterRepresentation
= {
193 group1__filter1
: false,
194 group1__filter2
: false,
195 group1__filter3
: false,
196 group2__filter4
: false,
197 group2__filter5
: false,
198 group2__filter6
: false,
199 group3__filter7
: false,
200 group3__filter8
: false,
201 group3__filter9
: false,
202 // The 'single_value' type of group can't have empty value; it's either
203 // the default given or the first item that will get the truthy value
204 group4__option1
: false,
205 group4__option2
: true, // Default
206 group4__option3
: false,
207 group5__option1
: true, // No default set, first item is default value
208 group5__option2
: false,
209 group5__option3
: false,
210 group6__group6option1
: false,
211 group6__group6option2
: true,
212 group6__group6option3
: true,
213 group7__group7option1
: false,
214 group7__group7option2
: true,
215 group7__group7option3
: false,
221 baseFullFilterState
= {
222 group1__filter1
: { selected
: false, conflicted
: false, included
: false },
223 group1__filter2
: { selected
: false, conflicted
: false, included
: false },
224 group1__filter3
: { selected
: false, conflicted
: false, included
: false },
225 group2__filter4
: { selected
: false, conflicted
: false, included
: false },
226 group2__filter5
: { selected
: false, conflicted
: false, included
: false },
227 group2__filter6
: { selected
: false, conflicted
: false, included
: false },
228 group3__filter7
: { selected
: false, conflicted
: false, included
: false },
229 group3__filter8
: { selected
: false, conflicted
: false, included
: false },
230 group3__filter9
: { selected
: false, conflicted
: false, included
: false },
231 group4__option1
: { selected
: false, conflicted
: false, included
: false },
232 group4__option2
: { selected
: true, conflicted
: false, included
: false },
233 group4__option3
: { selected
: false, conflicted
: false, included
: false },
234 group5__option1
: { selected
: true, conflicted
: false, included
: false },
235 group5__option2
: { selected
: false, conflicted
: false, included
: false },
236 group5__option3
: { selected
: false, conflicted
: false, included
: false },
237 group6__group6option1
: { selected
: false, conflicted
: false, included
: false },
238 group6__group6option2
: { selected
: true, conflicted
: false, included
: false },
239 group6__group6option3
: { selected
: true, conflicted
: false, included
: false },
240 group7__group7option1
: { selected
: false, conflicted
: false, included
: false },
241 group7__group7option2
: { selected
: true, conflicted
: false, included
: false },
242 group7__group7option3
: { selected
: false, conflicted
: false, included
: false },
243 namespace__0
: { selected
: false, conflicted
: false, included
: false },
244 namespace__1
: { selected
: false, conflicted
: false, included
: false },
245 namespace__2
: { selected
: false, conflicted
: false, included
: false },
246 namespace__3
: { selected
: false, conflicted
: false, included
: false }
249 QUnit
.module( 'mediawiki.rcfilters - FiltersViewModel', QUnit
.newMwEnvironment( {
251 'group1filter1-label': 'Group 1: Filter 1 title',
252 'group1filter1-desc': 'Description of Filter 1 in Group 1',
253 'group1filter2-label': 'Group 1: Filter 2 title',
254 'group1filter2-desc': 'Description of Filter 2 in Group 1',
255 'group1filter3-label': 'Group 1: Filter 3',
256 'group1filter3-desc': 'Description of Filter 3 in Group 1',
258 'group2filter4-label': 'Group 2: Filter 4 title',
259 'group2filter4-desc': 'Description of Filter 4 in Group 2',
260 'group2filter5-label': 'Group 2: Filter 5',
261 'group2filter5-desc': 'Description of Filter 5 in Group 2',
262 'group2filter6-label': 'xGroup 2: Filter 6',
263 'group2filter6-desc': 'Description of Filter 6 in Group 2'
267 QUnit
.test( 'Setting up filters', function ( assert
) {
268 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
270 model
.initializeFilters( filterDefinition
, viewsDefinition
);
272 // Test that all items were created
274 Object
.keys( baseFilterRepresentation
).every( function ( filterName
) {
275 return model
.getItemByName( filterName
) instanceof mw
.rcfilters
.dm
.FilterItem
;
277 'Filters instantiated and stored correctly'
281 model
.getSelectedState(),
282 baseFilterRepresentation
,
283 'Initial state of filters'
286 model
.toggleFiltersSelected( {
287 group1__filter1
: true,
288 group2__filter5
: true,
289 group3__filter7
: true
292 model
.getSelectedState(),
293 $.extend( true, {}, baseFilterRepresentation
, {
294 group1__filter1
: true,
295 group2__filter5
: true,
296 group3__filter7
: true
298 'Updating filter states correctly'
302 QUnit
.test( 'Default filters', function ( assert
) {
303 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
305 model
.initializeFilters( filterDefinition
, viewsDefinition
);
307 // Empty query = only default values
309 model
.getDefaultParams(),
311 'Default parameters are stored properly per filter and group'
314 // Change sticky filter
315 model
.toggleFiltersSelected( {
316 group7__group7option1
: true
319 // Make sure defaults have changed
321 model
.getDefaultParams(),
322 $.extend( true, {}, defaultParameters
, {
323 group7
: 'group7option1'
325 'Default parameters are stored properly per filter and group'
329 QUnit
.test( 'Parameter minimal state', function ( assert
) {
330 var model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
335 msg
: 'Empty parameter representation produces an empty result'
349 msg
: 'Mixed input results in only non-falsey values as result'
358 group1__filter1_color
: null
361 msg
: 'An all-falsey input results in an empty result.'
370 group1__filter1_color
: 'c1'
373 group1__filter1_color
: 'c1'
375 msg
: 'An all-falsey input with highlight params result in only the highlight param.'
379 group1__filter1_color
: 'c1',
380 group1__filter3_color
: 'c3' // Not supporting highlights
383 group1__filter1_color
: 'c1'
385 msg
: 'Unsupported highlights are removed.'
389 model
.initializeFilters( filterDefinition
, viewsDefinition
);
391 cases
.forEach( function ( test
) {
393 model
.getMinimizedParamRepresentation( test
.input
),
400 QUnit
.test( 'Parameter states', function ( assert
) {
401 // Some groups / params have their defaults immediately applied
402 // to their state. These include single_option which can never
403 // be empty, etc. These are these states:
404 var parametersWithoutExcluded
,
405 appliedDefaultParameters
= {
408 // Sticky, their defaults apply immediately
411 group7
: 'group7option2'
413 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
415 model
.initializeFilters( filterDefinition
, viewsDefinition
);
417 model
.getEmptyParameterState(),
418 emptyParamRepresentation
,
419 'Producing an empty parameter state'
422 model
.toggleFiltersSelected( {
423 group1__filter1
: true,
424 group3__filter7
: true
428 model
.getCurrentParameterState(),
429 // appliedDefaultParams applies the default value to parameters
430 // who must have an initial value to begin with, so we have to
431 // take it into account in the current state
432 $.extend( true, {}, appliedDefaultParameters
, {
437 'Producing a current parameter state'
441 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
442 model
.initializeFilters( filterDefinition
, viewsDefinition
);
444 parametersWithoutExcluded
= $.extend( true, {}, appliedDefaultParameters
);
445 delete parametersWithoutExcluded
.group7
;
446 delete parametersWithoutExcluded
.group6option2
;
447 delete parametersWithoutExcluded
.group6option3
;
450 model
.getCurrentParameterState( true ),
451 parametersWithoutExcluded
,
452 'Producing a current clean parameter state without excluded filters'
456 QUnit
.test( 'Cleaning up parameter states', function ( assert
) {
457 var model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
462 msg
: 'Empty parameter representation produces an empty result'
466 filter1
: '1', // Regular (do not strip)
467 group6option1
: '1', // Sticky
468 filter4
: '1', // Excluded
469 filter5
: '0' // Excluded
471 result
: { filter1
: '1' },
472 msg
: 'Valid input strips all sticky and excluded params regardless of value'
476 model
.initializeFilters( filterDefinition
, viewsDefinition
);
478 cases
.forEach( function ( test
) {
480 model
.removeExcludedParams( test
.input
),
488 QUnit
.test( 'Finding matching filters', function ( assert
) {
494 group1
: [ 'group1__filter1', 'group1__filter2', 'group1__filter3' ],
495 group2
: [ 'group2__filter4', 'group2__filter5' ]
497 reason
: 'Finds filters starting with the query string'
502 group2
: [ 'group2__filter4', 'group2__filter5', 'group2__filter6' ]
504 reason
: 'Finds filters containing the query string in their description'
509 group1
: [ 'group1__filter1', 'group1__filter2' ],
510 group2
: [ 'group2__filter4' ]
512 reason
: 'Finds filters containing the query string in their group title'
517 namespace: [ 'namespace__0' ]
519 reason
: 'Finds item in view when a prefix is used'
524 reason
: 'Finds no results if using namespaces prefix (:) to search for filter title'
527 model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
528 extractNames = function ( matches
) {
530 Object
.keys( matches
).forEach( function ( groupName
) {
531 result
[ groupName
] = matches
[ groupName
].map( function ( item
) {
532 return item
.getName();
538 model
.initializeFilters( filterDefinition
, viewsDefinition
);
540 testCases
.forEach( function ( testCase
) {
541 matches
= model
.findMatches( testCase
.query
);
543 extractNames( matches
),
544 testCase
.expectedMatches
,
549 matches
= model
.findMatches( 'foo' );
551 $.isEmptyObject( matches
),
552 'findMatches returns an empty object when no results found'
556 QUnit
.test( 'getParametersFromFilters', function ( assert
) {
557 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
559 model
.initializeFilters( filterDefinition
, viewsDefinition
);
561 // Starting with all filters unselected
563 model
.getParametersFromFilters(),
564 baseParamRepresentation
,
565 'Unselected filters return all parameters falsey or \'\'.'
569 model
.toggleFiltersSelected( {
570 group1__filter1
: true
572 // Only one filter in one group
574 model
.getParametersFromFilters(),
575 $.extend( true, {}, baseParamRepresentation
, {
576 // Group 1 (one selected, the others are true)
580 'One filter in one "send_unselected_if_any" group returns the other parameters truthy.'
584 model
.toggleFiltersSelected( {
585 group1__filter1
: true,
586 group1__filter2
: true
588 // Two selected filters in one group
590 model
.getParametersFromFilters(),
591 $.extend( true, {}, baseParamRepresentation
, {
592 // Group 1 (two selected, the other is true)
595 'Two filters in one "send_unselected_if_any" group returns the other parameters truthy.'
599 model
.toggleFiltersSelected( {
600 group1__filter1
: true,
601 group1__filter2
: true,
602 group1__filter3
: true
604 // All filters of the group are selected == this is the same as not selecting any
606 model
.getParametersFromFilters(),
607 baseParamRepresentation
,
608 'All filters selected in one "send_unselected_if_any" group returns all parameters falsy.'
611 // Select 1 filter from string_options
612 model
.toggleFiltersSelected( {
613 group3__filter7
: true,
614 group3__filter8
: false,
615 group3__filter9
: false
617 // All filters of the group are selected == this is the same as not selecting any
619 model
.getParametersFromFilters(),
620 $.extend( true, {}, baseParamRepresentation
, {
623 'One filter selected in "string_option" group returns that filter in the value.'
626 // Select 2 filters from string_options
627 model
.toggleFiltersSelected( {
628 group3__filter7
: true,
629 group3__filter8
: true,
630 group3__filter9
: false
632 // All filters of the group are selected == this is the same as not selecting any
634 model
.getParametersFromFilters(),
635 $.extend( true, {}, baseParamRepresentation
, {
636 group3
: 'filter7,filter8'
638 'Two filters selected in "string_option" group returns those filters in the value.'
641 // Select 3 filters from string_options
642 model
.toggleFiltersSelected( {
643 group3__filter7
: true,
644 group3__filter8
: true,
645 group3__filter9
: true
647 // All filters of the group are selected == this is the same as not selecting any
649 model
.getParametersFromFilters(),
650 $.extend( true, {}, baseParamRepresentation
, {
653 'All filters selected in "string_option" group returns \'all\'.'
657 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
658 model
.initializeFilters( filterDefinition
, viewsDefinition
);
660 // Select an option from single_option group
661 model
.toggleFiltersSelected( {
662 group4__option2
: true
664 // All filters of the group are selected == this is the same as not selecting any
666 model
.getParametersFromFilters(),
667 $.extend( true, {}, baseParamRepresentation
, {
670 'Selecting an option from "single_option" group returns that option as a value.'
673 // Select a different option from single_option group
674 model
.toggleFiltersSelected( {
675 group4__option3
: true
677 // All filters of the group are selected == this is the same as not selecting any
679 model
.getParametersFromFilters(),
680 $.extend( true, {}, baseParamRepresentation
, {
683 'Selecting a different option from "single_option" group changes the selection.'
687 QUnit
.test( 'getParametersFromFilters (custom object)', function ( assert
) {
688 // This entire test uses different base definition than the global one
689 // on purpose, to verify that the values inserted as a custom object
690 // are the ones we expect in return
692 model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
696 type
: 'send_unselected_if_any',
698 { name
: 'hidefilter1', label
: 'Hide filter 1', description
: '' },
699 { name
: 'hidefilter2', label
: 'Hide filter 2', description
: '' },
700 { name
: 'hidefilter3', label
: 'Hide filter 3', description
: '' }
705 type
: 'send_unselected_if_any',
707 { name
: 'hidefilter4', label
: 'Hide filter 4', description
: '' },
708 { name
: 'hidefilter5', label
: 'Hide filter 5', description
: '' },
709 { name
: 'hidefilter6', label
: 'Hide filter 6', description
: '' }
714 type
: 'string_options',
717 { name
: 'filter7', label
: 'Hide filter 7', description
: '' },
718 { name
: 'filter8', label
: 'Hide filter 8', description
: '' },
719 { name
: 'filter9', label
: 'Hide filter 9', description
: '' }
724 type
: 'single_option',
726 { name
: 'filter10', label
: 'Hide filter 10', description
: '' },
727 { name
: 'filter11', label
: 'Hide filter 11', description
: '' },
728 { name
: 'filter12', label
: 'Hide filter 12', description
: '' }
743 // This is mocking the cases above, both
744 // - 'Two filters in one "send_unselected_if_any" group returns the other parameters truthy.'
745 // - 'Two filters selected in "string_option" group returns those filters in the value.'
747 group1__hidefilter1
: true,
748 group1__hidefilter2
: true,
749 group1__hidefilter3
: false,
750 group2__hidefilter4
: false,
751 group2__hidefilter5
: false,
752 group2__hidefilter6
: false,
753 group3__filter7
: true,
754 group3__filter8
: true,
755 group3__filter9
: false
757 expected
: $.extend( true, {}, baseResult
, {
758 // Group 1 (two selected, the others are true)
760 // Group 3 (two selected)
761 group3
: 'filter7,filter8'
763 msg
: 'Given an explicit (complete) filter state object, the result is the same as if the object given represented the model state.'
766 // This is mocking case above
767 // - 'One filter in one "send_unselected_if_any" group returns the other parameters truthy.'
769 group1__hidefilter1
: 1
771 expected
: $.extend( true, {}, baseResult
, {
772 // Group 1 (one selected, the others are true)
776 msg
: 'Given an explicit (incomplete) filter state object, the result is the same as if the object give represented the model state.'
780 group4__filter10
: true
782 expected
: $.extend( true, {}, baseResult
, {
785 msg
: 'Given a single value for "single_option" that option is represented in the result.'
789 group4__filter10
: true,
790 group4__filter11
: true
792 expected
: $.extend( true, {}, baseResult
, {
795 msg
: 'Given more than one true value for "single_option" (which should not happen!) only the first value counts, and the second is ignored.'
799 expected
: baseResult
,
800 msg
: 'Given an explicit empty object, the result is all filters set to their falsey unselected value.'
804 model
.initializeFilters( definition
);
805 // Store original state
806 originalState
= model
.getSelectedState();
809 cases
.forEach( function ( test
) {
811 model
.getParametersFromFilters( test
.input
),
817 // After doing the above tests, make sure the actual state
818 // of the filter stayed the same
820 model
.getSelectedState(),
822 'Running the method with external definition to parse does not actually change the state of the model'
826 QUnit
.test( 'getFiltersFromParameters', function ( assert
) {
827 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
829 model
.initializeFilters( filterDefinition
, viewsDefinition
);
831 // Empty query = only default values
833 model
.getFiltersFromParameters( {} ),
834 baseFilterRepresentation
,
835 'Empty parameter query results in an object representing all filters set to their base state'
839 model
.getFiltersFromParameters( {
842 $.extend( {}, baseFilterRepresentation
, {
843 group1__filter1
: true, // The text is "show filter 1"
844 group1__filter2
: false, // The text is "show filter 2"
845 group1__filter3
: true // The text is "show filter 3"
847 'One truthy parameter in a group whose other parameters are true by default makes the rest of the filters in the group false (unchecked)'
851 model
.getFiltersFromParameters( {
856 $.extend( {}, baseFilterRepresentation
, {
857 group1__filter1
: false, // The text is "show filter 1"
858 group1__filter2
: false, // The text is "show filter 2"
859 group1__filter3
: false // The text is "show filter 3"
861 'All paremeters in the same \'send_unselected_if_any\' group false is equivalent to none are truthy (checked) in the interface'
864 // The ones above don't update the model, so we have a clean state.
865 // getFiltersFromParameters is stateless; any change is unaffected by the current state
866 // This test is demonstrating wrong usage of the method;
867 // We should be aware that getFiltersFromParameters is stateless,
868 // so each call gives us a filter state that only reflects the query given.
869 // This means that the two calls to toggleFiltersSelected() below collide.
870 // The result of the first is overridden by the result of the second,
871 // since both get a full state object from getFiltersFromParameters that **only** relates
872 // to the input it receives.
873 model
.toggleFiltersSelected(
874 model
.getFiltersFromParameters( {
879 model
.toggleFiltersSelected(
880 model
.getFiltersFromParameters( {
885 // The result here is ignoring the first toggleFiltersSelected call
887 model
.getSelectedState(),
888 $.extend( {}, baseFilterRepresentation
, {
889 group2__filter4
: true,
890 group2__filter5
: true,
891 group2__filter6
: false
893 'getFiltersFromParameters does not care about previous or existing state.'
897 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
898 model
.initializeFilters( filterDefinition
, viewsDefinition
);
900 model
.toggleFiltersSelected(
901 model
.getFiltersFromParameters( {
906 model
.getSelectedState(),
907 $.extend( {}, baseFilterRepresentation
, {
908 group3__filter7
: true,
909 group3__filter8
: false,
910 group3__filter9
: false
912 'A \'string_options\' parameter containing 1 value, results in the corresponding filter as checked'
915 model
.toggleFiltersSelected(
916 model
.getFiltersFromParameters( {
917 group3
: 'filter7,filter8'
921 model
.getSelectedState(),
922 $.extend( {}, baseFilterRepresentation
, {
923 group3__filter7
: true,
924 group3__filter8
: true,
925 group3__filter9
: false
927 'A \'string_options\' parameter containing 2 values, results in both corresponding filters as checked'
930 model
.toggleFiltersSelected(
931 model
.getFiltersFromParameters( {
932 group3
: 'filter7,filter8,filter9'
936 model
.getSelectedState(),
937 $.extend( {}, baseFilterRepresentation
, {
938 group3__filter7
: true,
939 group3__filter8
: true,
940 group3__filter9
: true
942 'A \'string_options\' parameter containing all values, results in all filters of the group as checked.'
945 model
.toggleFiltersSelected(
946 model
.getFiltersFromParameters( {
947 group3
: 'filter7,all,filter9'
951 model
.getSelectedState(),
952 $.extend( {}, baseFilterRepresentation
, {
953 group3__filter7
: true,
954 group3__filter8
: true,
955 group3__filter9
: true
957 'A \'string_options\' parameter containing the value \'all\', results in all filters of the group as checked.'
960 model
.toggleFiltersSelected(
961 model
.getFiltersFromParameters( {
962 group3
: 'filter7,foo,filter9'
966 model
.getSelectedState(),
967 $.extend( {}, baseFilterRepresentation
, {
968 group3__filter7
: true,
969 group3__filter8
: false,
970 group3__filter9
: true
972 'A \'string_options\' parameter containing an invalid value, results in the invalid value ignored and the valid corresponding filters checked.'
975 model
.toggleFiltersSelected(
976 model
.getFiltersFromParameters( {
981 model
.getSelectedState(),
982 $.extend( {}, baseFilterRepresentation
, {
983 group4__option1
: true,
984 group4__option2
: false
986 'A \'single_option\' parameter reflects a single selected value.'
990 model
.getFiltersFromParameters( {
991 group4
: 'option1,option2'
993 baseFilterRepresentation
,
994 'An invalid \'single_option\' parameter is ignored.'
997 // Change to one value
998 model
.toggleFiltersSelected(
999 model
.getFiltersFromParameters( {
1003 // Change again to another value
1004 model
.toggleFiltersSelected(
1005 model
.getFiltersFromParameters( {
1010 model
.getSelectedState(),
1011 $.extend( {}, baseFilterRepresentation
, {
1012 group4__option2
: true
1014 'A \'single_option\' parameter always reflects the latest selected value.'
1018 QUnit
.test( 'sanitizeStringOptionGroup', function ( assert
) {
1019 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1021 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1024 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'filter1', 'filter2' ] ),
1025 [ 'filter1', 'filter2' ],
1026 'Remove duplicate values'
1030 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'foo', 'filter2' ] ),
1031 [ 'filter1', 'filter2' ],
1032 'Remove invalid values'
1036 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'all', 'filter2' ] ),
1038 'If any value is "all", the only value is "all".'
1042 QUnit
.test( 'Filter interaction: subsets', function ( assert
) {
1043 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1045 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1047 // Select a filter that has subset with another filter
1048 model
.toggleFiltersSelected( {
1049 group1__filter1
: true
1052 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
1054 model
.getFullState(),
1055 $.extend( true, {}, baseFullFilterState
, {
1056 group1__filter1
: { selected
: true },
1057 group1__filter2
: { included
: true },
1058 group1__filter3
: { included
: true },
1059 // Conflicts are affected
1060 group2__filter4
: { conflicted
: true },
1061 group2__filter5
: { conflicted
: true },
1062 group2__filter6
: { conflicted
: true }
1064 'Filters with subsets are represented in the model.'
1067 // Select another filter that has a subset with the same previous filter
1068 model
.toggleFiltersSelected( {
1069 group1__filter2
: true
1071 model
.reassessFilterInteractions( model
.getItemByName( 'filter2' ) );
1073 model
.getFullState(),
1074 $.extend( true, {}, baseFullFilterState
, {
1075 group1__filter1
: { selected
: true },
1076 group1__filter2
: { selected
: true, included
: true },
1077 group1__filter3
: { included
: true },
1078 // Conflicts are affected
1079 group2__filter6
: { conflicted
: true }
1081 'Filters that have multiple subsets are represented.'
1084 // Remove one filter (but leave the other) that affects filter3
1085 model
.toggleFiltersSelected( {
1086 group1__filter1
: false
1088 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
1090 model
.getFullState(),
1091 $.extend( true, {}, baseFullFilterState
, {
1092 group1__filter2
: { selected
: true, included
: false },
1093 group1__filter3
: { included
: true },
1094 // Conflicts are affected
1095 group2__filter6
: { conflicted
: true }
1097 'Removing a filter only un-includes its subset if there is no other filter affecting.'
1100 model
.toggleFiltersSelected( {
1101 group1__filter2
: false
1103 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1105 model
.getFullState(),
1106 baseFullFilterState
,
1107 'Removing all supersets also un-includes the subsets.'
1111 QUnit
.test( 'Filter interaction: full coverage', function ( assert
) {
1112 var model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
1113 isCapsuleItemMuted = function ( filterName
) {
1114 var itemModel
= model
.getItemByName( filterName
),
1115 groupModel
= itemModel
.getGroupModel();
1117 // This is the logic inside the capsule widget
1119 // The capsule item widget only appears if the item is selected
1120 itemModel
.isSelected() &&
1121 // Muted state is only valid if group is full coverage and all items are selected
1122 groupModel
.isFullCoverage() && groupModel
.areAllSelected()
1125 getCurrentItemsMutedState = function () {
1127 group1__filter1
: isCapsuleItemMuted( 'group1__filter1' ),
1128 group1__filter2
: isCapsuleItemMuted( 'group1__filter2' ),
1129 group1__filter3
: isCapsuleItemMuted( 'group1__filter3' ),
1130 group2__filter4
: isCapsuleItemMuted( 'group2__filter4' ),
1131 group2__filter5
: isCapsuleItemMuted( 'group2__filter5' ),
1132 group2__filter6
: isCapsuleItemMuted( 'group2__filter6' )
1136 group1__filter1
: false,
1137 group1__filter2
: false,
1138 group1__filter3
: false,
1139 group2__filter4
: false,
1140 group2__filter5
: false,
1141 group2__filter6
: false
1144 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1146 // Starting state, no selection, all items are non-muted
1148 getCurrentItemsMutedState(),
1150 'No selection - all items are non-muted'
1153 // Select most (but not all) items in each group
1154 model
.toggleFiltersSelected( {
1155 group1__filter1
: true,
1156 group1__filter2
: true,
1157 group2__filter4
: true,
1158 group2__filter5
: true
1161 // Both groups have multiple (but not all) items selected, all items are non-muted
1163 getCurrentItemsMutedState(),
1165 'Not all items in the group selected - all items are non-muted'
1168 // Select all items in 'fullCoverage' group (group2)
1169 model
.toggleFiltersSelected( {
1170 group2__filter6
: true
1173 // Group2 (full coverage) has all items selected, all its items are muted
1175 getCurrentItemsMutedState(),
1176 $.extend( {}, baseMuteState
, {
1177 group2__filter4
: true,
1178 group2__filter5
: true,
1179 group2__filter6
: true
1181 'All items in \'full coverage\' group are selected - all items in the group are muted'
1184 // Select all items in non 'fullCoverage' group (group1)
1185 model
.toggleFiltersSelected( {
1186 group1__filter3
: true
1189 // Group1 (full coverage) has all items selected, no items in it are muted (non full coverage)
1191 getCurrentItemsMutedState(),
1192 $.extend( {}, baseMuteState
, {
1193 group2__filter4
: true,
1194 group2__filter5
: true,
1195 group2__filter6
: true
1197 'All items in a non \'full coverage\' group are selected - none of the items in the group are muted'
1200 // Uncheck an item from each group
1201 model
.toggleFiltersSelected( {
1202 group1__filter3
: false,
1203 group2__filter5
: false
1206 getCurrentItemsMutedState(),
1208 'Not all items in the group are checked - all items are non-muted regardless of group coverage'
1212 QUnit
.test( 'Filter interaction: conflicts', function ( assert
) {
1213 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1215 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1218 model
.getFullState(),
1219 baseFullFilterState
,
1220 'Initial state: no conflicts because no selections.'
1223 // Select a filter that has a conflict with an entire group
1224 model
.toggleFiltersSelected( {
1225 group1__filter1
: true // conflicts: entire of group 2 ( filter4, filter5, filter6)
1228 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
1231 model
.getFullState(),
1232 $.extend( true, {}, baseFullFilterState
, {
1233 group1__filter1
: { selected
: true },
1234 group2__filter4
: { conflicted
: true },
1235 group2__filter5
: { conflicted
: true },
1236 group2__filter6
: { conflicted
: true },
1237 // Subsets are affected by the selection
1238 group1__filter2
: { included
: true },
1239 group1__filter3
: { included
: true }
1241 'Selecting a filter that conflicts with a group sets all the conflicted group items as "conflicted".'
1244 // Select one of the conflicts (both filters are now conflicted and selected)
1245 model
.toggleFiltersSelected( {
1246 group2__filter4
: true // conflicts: filter 1
1248 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter4' ) );
1251 model
.getFullState(),
1252 $.extend( true, {}, baseFullFilterState
, {
1253 group1__filter1
: { selected
: true, conflicted
: true },
1254 group2__filter4
: { selected
: true, conflicted
: true },
1255 group2__filter5
: { conflicted
: true },
1256 group2__filter6
: { conflicted
: true },
1257 // Subsets are affected by the selection
1258 group1__filter2
: { included
: true },
1259 group1__filter3
: { included
: true }
1261 'Selecting a conflicting filter inside a group, sets both sides to conflicted and selected.'
1265 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1266 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1268 // Select a filter that has a conflict with a specific filter
1269 model
.toggleFiltersSelected( {
1270 group1__filter2
: true // conflicts: filter6
1272 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1275 model
.getFullState(),
1276 $.extend( true, {}, baseFullFilterState
, {
1277 group1__filter2
: { selected
: true },
1278 group2__filter6
: { conflicted
: true },
1279 // Subsets are affected by the selection
1280 group1__filter3
: { included
: true }
1282 'Selecting a filter that conflicts with another filter sets the other as "conflicted".'
1285 // Select the conflicting filter
1286 model
.toggleFiltersSelected( {
1287 group2__filter6
: true // conflicts: filter2
1290 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter6' ) );
1293 model
.getFullState(),
1294 $.extend( true, {}, baseFullFilterState
, {
1295 group1__filter2
: { selected
: true, conflicted
: true },
1296 group2__filter6
: { selected
: true, conflicted
: true },
1297 // This is added to the conflicts because filter6 is part of group2,
1298 // who is in conflict with filter1; note that filter2 also conflicts
1299 // with filter6 which means that filter1 conflicts with filter6 (because it's in group2)
1300 // and also because its **own sibling** (filter2) is **also** in conflict with the
1301 // selected items in group2 (filter6)
1302 group1__filter1
: { conflicted
: true },
1304 // Subsets are affected by the selection
1305 group1__filter3
: { included
: true }
1307 'Selecting a conflicting filter with an individual filter, sets both sides to conflicted and selected.'
1310 // Now choose a non-conflicting filter from the group
1311 model
.toggleFiltersSelected( {
1312 group2__filter5
: true
1315 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter5' ) );
1318 model
.getFullState(),
1319 $.extend( true, {}, baseFullFilterState
, {
1320 group1__filter2
: { selected
: true },
1321 group2__filter6
: { selected
: true },
1322 group2__filter5
: { selected
: true },
1323 // Filter6 and filter1 are no longer in conflict because
1324 // filter5, while it is in conflict with filter1, it is
1325 // not in conflict with filter2 - and since filter2 is
1326 // selected, it removes the conflict bidirectionally
1328 // Subsets are affected by the selection
1329 group1__filter3
: { included
: true }
1331 'Selecting a non-conflicting filter within the group of a conflicting filter removes the conflicts.'
1334 // Followup on the previous test, unselect filter2 so filter1
1335 // is now the only one selected in its own group, and since
1336 // it is in conflict with the entire of group2, it means
1337 // filter1 is once again conflicted
1338 model
.toggleFiltersSelected( {
1339 group1__filter2
: false
1342 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1345 model
.getFullState(),
1346 $.extend( true, {}, baseFullFilterState
, {
1347 group1__filter1
: { conflicted
: true },
1348 group2__filter6
: { selected
: true },
1349 group2__filter5
: { selected
: true }
1351 'Unselecting an item that did not conflict returns the conflict state.'
1354 // Followup #2: Now actually select filter1, and make everything conflicted
1355 model
.toggleFiltersSelected( {
1356 group1__filter1
: true
1359 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
1362 model
.getFullState(),
1363 $.extend( true, {}, baseFullFilterState
, {
1364 group1__filter1
: { selected
: true, conflicted
: true },
1365 group2__filter6
: { selected
: true, conflicted
: true },
1366 group2__filter5
: { selected
: true, conflicted
: true },
1367 group2__filter4
: { conflicted
: true }, // Not selected but conflicted because it's in group2
1368 // Subsets are affected by the selection
1369 group1__filter2
: { included
: true },
1370 group1__filter3
: { included
: true }
1372 'Selecting an item that conflicts with a whole group makes all selections in that group conflicted.'
1377 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1378 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1380 // Select a filter that has a conflict with a specific filter
1381 model
.toggleFiltersSelected( {
1382 group1__filter2
: true // conflicts: filter6
1385 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1388 model
.getFullState(),
1389 $.extend( true, {}, baseFullFilterState
, {
1390 group1__filter2
: { selected
: true },
1391 group2__filter6
: { conflicted
: true },
1392 // Subsets are affected by the selection
1393 group1__filter3
: { included
: true }
1395 'Simple case: Selecting a filter that conflicts with another filter sets the other as "conflicted".'
1398 model
.toggleFiltersSelected( {
1399 group1__filter3
: true // conflicts: filter6
1402 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter3' ) );
1405 model
.getFullState(),
1406 $.extend( true, {}, baseFullFilterState
, {
1407 group1__filter2
: { selected
: true },
1408 // Subsets are affected by the selection
1409 group1__filter3
: { selected
: true, included
: true }
1411 'Simple case: Selecting a filter that is not in conflict removes the conflict.'
1415 QUnit
.test( 'Filter highlights', function ( assert
) {
1416 // We are using a different (smaller) definition here than the global one
1417 var definition
= [ {
1420 type
: 'string_options',
1422 { name
: 'filter1', cssClass
: 'class1', label
: '1', description
: '1' },
1423 { name
: 'filter2', cssClass
: 'class2', label
: '2', description
: '2' },
1424 { name
: 'filter3', cssClass
: 'class3', label
: '3', description
: '3' },
1425 { name
: 'filter4', cssClass
: 'class4', label
: '4', description
: '4' },
1426 { name
: 'filter5', cssClass
: 'class5', label
: '5', description
: '5' },
1427 { name
: 'filter6', label
: '6', description
: '6' }
1430 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1432 model
.initializeFilters( definition
);
1435 !model
.isHighlightEnabled(),
1436 'Initially, highlight is disabled.'
1439 model
.toggleHighlight( true );
1441 model
.isHighlightEnabled(),
1442 'Highlight is enabled on toggle.'
1445 model
.setHighlightColor( 'group1__filter1', 'color1' );
1446 model
.setHighlightColor( 'group1__filter2', 'color2' );
1449 model
.getHighlightedItems().map( function ( item
) {
1450 return item
.getName();
1456 'Highlighted items are highlighted.'
1460 model
.getItemByName( 'group1__filter1' ).getHighlightColor(),
1462 'Item highlight color is set.'
1465 model
.setHighlightColor( 'group1__filter1', 'color1changed' );
1467 model
.getItemByName( 'group1__filter1' ).getHighlightColor(),
1469 'Item highlight color is changed on setHighlightColor.'
1472 model
.clearHighlightColor( 'group1__filter1' );
1474 model
.getHighlightedItems().map( function ( item
) {
1475 return item
.getName();
1480 'Clear highlight from an item results in the item no longer being highlighted.'
1484 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1485 model
.initializeFilters( definition
);
1487 model
.setHighlightColor( 'group1__filter1', 'color1' );
1488 model
.setHighlightColor( 'group1__filter2', 'color2' );
1489 model
.setHighlightColor( 'group1__filter3', 'color3' );
1492 model
.getHighlightedItems().map( function ( item
) {
1493 return item
.getName();
1500 'Even if highlights are not enabled, the items remember their highlight state'
1501 // NOTE: When actually displaying the highlights, the UI checks whether
1502 // highlighting is generally active and then goes over the highlighted
1503 // items. The item models, however, and the view model in general, still
1504 // retains the knowledge about which filters have different colors, so we
1505 // can seamlessly return to the colors the user previously chose if they
1506 // reapply highlights.
1510 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1511 model
.initializeFilters( definition
);
1513 model
.setHighlightColor( 'group1__filter1', 'color1' );
1514 model
.setHighlightColor( 'group1__filter6', 'color6' );
1517 model
.getHighlightedItems().map( function ( item
) {
1518 return item
.getName();
1523 'Items without a specified class identifier are not highlighted.'
1526 }( mediaWiki
, jQuery
) );