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
= {
165 group1__filter1_color
: null,
166 group1__filter2_color
: null,
167 // group1__filter3_color: null, // Highlight isn't supported
168 group2__filter4_color
: null,
169 group2__filter5_color
: null,
170 group2__filter6_color
: null,
171 group3__filter7_color
: null,
172 group3__filter8_color
: null,
173 group3__filter9_color
: null,
174 // group4__option1_color: null, // Highlight isn't supported
175 // group4__option2_color: null, // Highlight isn't supported
176 // group4__option3_color: null, // Highlight isn't supported
177 group5__option1_color
: null,
178 group5__option2_color
: null,
179 group5__option3_color
: null,
180 group6__group6option1_color
: null,
181 group6__group6option2_color
: null,
182 group6__group6option3_color
: null,
183 group7__group7option1_color
: null,
184 group7__group7option2_color
: null,
185 group7__group7option3_color
: null,
186 namespace__0_color
: null,
187 namespace__1_color
: null,
188 namespace__2_color
: null,
189 namespace__3_color
: null
191 baseFilterRepresentation
= {
192 group1__filter1
: false,
193 group1__filter2
: false,
194 group1__filter3
: false,
195 group2__filter4
: false,
196 group2__filter5
: false,
197 group2__filter6
: false,
198 group3__filter7
: false,
199 group3__filter8
: false,
200 group3__filter9
: false,
201 // The 'single_value' type of group can't have empty value; it's either
202 // the default given or the first item that will get the truthy value
203 group4__option1
: false,
204 group4__option2
: true, // Default
205 group4__option3
: false,
206 group5__option1
: true, // No default set, first item is default value
207 group5__option2
: false,
208 group5__option3
: false,
209 group6__group6option1
: false,
210 group6__group6option2
: true,
211 group6__group6option3
: true,
212 group7__group7option1
: false,
213 group7__group7option2
: true,
214 group7__group7option3
: false,
220 baseFullFilterState
= {
221 group1__filter1
: { selected
: false, conflicted
: false, included
: false },
222 group1__filter2
: { selected
: false, conflicted
: false, included
: false },
223 group1__filter3
: { selected
: false, conflicted
: false, included
: false },
224 group2__filter4
: { selected
: false, conflicted
: false, included
: false },
225 group2__filter5
: { selected
: false, conflicted
: false, included
: false },
226 group2__filter6
: { selected
: false, conflicted
: false, included
: false },
227 group3__filter7
: { selected
: false, conflicted
: false, included
: false },
228 group3__filter8
: { selected
: false, conflicted
: false, included
: false },
229 group3__filter9
: { selected
: false, conflicted
: false, included
: false },
230 group4__option1
: { selected
: false, conflicted
: false, included
: false },
231 group4__option2
: { selected
: true, conflicted
: false, included
: false },
232 group4__option3
: { selected
: false, conflicted
: false, included
: false },
233 group5__option1
: { selected
: true, conflicted
: false, included
: false },
234 group5__option2
: { selected
: false, conflicted
: false, included
: false },
235 group5__option3
: { selected
: false, conflicted
: false, included
: false },
236 group6__group6option1
: { selected
: false, conflicted
: false, included
: false },
237 group6__group6option2
: { selected
: true, conflicted
: false, included
: false },
238 group6__group6option3
: { selected
: true, conflicted
: false, included
: false },
239 group7__group7option1
: { selected
: false, conflicted
: false, included
: false },
240 group7__group7option2
: { selected
: true, conflicted
: false, included
: false },
241 group7__group7option3
: { selected
: false, conflicted
: false, included
: false },
242 namespace__0
: { selected
: false, conflicted
: false, included
: false },
243 namespace__1
: { selected
: false, conflicted
: false, included
: false },
244 namespace__2
: { selected
: false, conflicted
: false, included
: false },
245 namespace__3
: { selected
: false, conflicted
: false, included
: false }
248 QUnit
.module( 'mediawiki.rcfilters - FiltersViewModel', QUnit
.newMwEnvironment( {
250 'group1filter1-label': 'Group 1: Filter 1 title',
251 'group1filter1-desc': 'Description of Filter 1 in Group 1',
252 'group1filter2-label': 'Group 1: Filter 2 title',
253 'group1filter2-desc': 'Description of Filter 2 in Group 1',
254 'group1filter3-label': 'Group 1: Filter 3',
255 'group1filter3-desc': 'Description of Filter 3 in Group 1',
257 'group2filter4-label': 'Group 2: Filter 4 title',
258 'group2filter4-desc': 'Description of Filter 4 in Group 2',
259 'group2filter5-label': 'Group 2: Filter 5',
260 'group2filter5-desc': 'Description of Filter 5 in Group 2',
261 'group2filter6-label': 'xGroup 2: Filter 6',
262 'group2filter6-desc': 'Description of Filter 6 in Group 2'
266 QUnit
.test( 'Setting up filters', function ( assert
) {
267 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
269 model
.initializeFilters( filterDefinition
, viewsDefinition
);
271 // Test that all items were created
273 Object
.keys( baseFilterRepresentation
).every( function ( filterName
) {
274 return model
.getItemByName( filterName
) instanceof mw
.rcfilters
.dm
.FilterItem
;
276 'Filters instantiated and stored correctly'
280 model
.getSelectedState(),
281 baseFilterRepresentation
,
282 'Initial state of filters'
285 model
.toggleFiltersSelected( {
286 group1__filter1
: true,
287 group2__filter5
: true,
288 group3__filter7
: true
291 model
.getSelectedState(),
292 $.extend( true, {}, baseFilterRepresentation
, {
293 group1__filter1
: true,
294 group2__filter5
: true,
295 group3__filter7
: true
297 'Updating filter states correctly'
301 QUnit
.test( 'Default filters', function ( assert
) {
302 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
304 model
.initializeFilters( filterDefinition
, viewsDefinition
);
306 // Empty query = only default values
308 model
.getDefaultParams(),
310 'Default parameters are stored properly per filter and group'
313 // Change sticky filter
314 model
.toggleFiltersSelected( {
315 group7__group7option1
: true
318 // Make sure defaults have changed
320 model
.getDefaultParams(),
321 $.extend( true, {}, defaultParameters
, {
322 group7
: 'group7option1'
324 'Default parameters are stored properly per filter and group'
328 QUnit
.test( 'Parameter minimal state', function ( assert
) {
329 var model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
334 msg
: 'Empty parameter representation produces an empty result'
348 msg
: 'Mixed input results in only non-falsey values as result'
357 group1__filter1_color
: null
360 msg
: 'An all-falsey input results in an empty result.'
369 group1__filter1_color
: 'c1'
372 group1__filter1_color
: 'c1'
374 msg
: 'An all-falsey input with highlight params result in only the highlight param.'
378 group1__filter1_color
: 'c1',
379 group1__filter3_color
: 'c3' // Not supporting highlights
382 group1__filter1_color
: 'c1'
384 msg
: 'Unsupported highlights are removed.'
388 model
.initializeFilters( filterDefinition
, viewsDefinition
);
390 cases
.forEach( function ( test
) {
392 model
.getMinimizedParamRepresentation( test
.input
),
399 QUnit
.test( 'Parameter states', function ( assert
) {
400 // Some groups / params have their defaults immediately applied
401 // to their state. These include single_option which can never
402 // be empty, etc. These are these states:
403 var parametersWithoutExcluded
,
404 appliedDefaultParameters
= {
407 // Sticky, their defaults apply immediately
410 group7
: 'group7option2'
412 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
414 model
.initializeFilters( filterDefinition
, viewsDefinition
);
416 model
.getEmptyParameterState(),
417 emptyParamRepresentation
,
418 'Producing an empty parameter state'
421 model
.toggleFiltersSelected( {
422 group1__filter1
: true,
423 group3__filter7
: true
427 model
.getCurrentParameterState(),
428 // appliedDefaultParams applies the default value to parameters
429 // who must have an initial value to begin with, so we have to
430 // take it into account in the current state
431 $.extend( true, {}, appliedDefaultParameters
, {
436 'Producing a current parameter state'
440 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
441 model
.initializeFilters( filterDefinition
, viewsDefinition
);
443 parametersWithoutExcluded
= $.extend( true, {}, appliedDefaultParameters
);
444 delete parametersWithoutExcluded
.group7
;
445 delete parametersWithoutExcluded
.group6option2
;
446 delete parametersWithoutExcluded
.group6option3
;
449 model
.getCurrentParameterState( true ),
450 parametersWithoutExcluded
,
451 'Producing a current clean parameter state without excluded filters'
455 QUnit
.test( 'Cleaning up parameter states', function ( assert
) {
456 var model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
461 msg
: 'Empty parameter representation produces an empty result'
465 filter1
: '1', // Regular (do not strip)
466 group6option1
: '1', // Sticky
467 filter4
: '1', // Excluded
468 filter5
: '0' // Excluded
470 result
: { filter1
: '1' },
471 msg
: 'Valid input strips all sticky and excluded params regardless of value'
475 model
.initializeFilters( filterDefinition
, viewsDefinition
);
477 cases
.forEach( function ( test
) {
479 model
.removeExcludedParams( test
.input
),
487 QUnit
.test( 'Finding matching filters', function ( assert
) {
493 group1
: [ 'group1__filter1', 'group1__filter2', 'group1__filter3' ],
494 group2
: [ 'group2__filter4', 'group2__filter5' ]
496 reason
: 'Finds filters starting with the query string'
501 group2
: [ 'group2__filter4', 'group2__filter5', 'group2__filter6' ]
503 reason
: 'Finds filters containing the query string in their description'
508 group1
: [ 'group1__filter1', 'group1__filter2' ],
509 group2
: [ 'group2__filter4' ]
511 reason
: 'Finds filters containing the query string in their group title'
516 namespace: [ 'namespace__0' ]
518 reason
: 'Finds item in view when a prefix is used'
523 reason
: 'Finds no results if using namespaces prefix (:) to search for filter title'
526 model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
527 extractNames = function ( matches
) {
529 Object
.keys( matches
).forEach( function ( groupName
) {
530 result
[ groupName
] = matches
[ groupName
].map( function ( item
) {
531 return item
.getName();
537 model
.initializeFilters( filterDefinition
, viewsDefinition
);
539 testCases
.forEach( function ( testCase
) {
540 matches
= model
.findMatches( testCase
.query
);
542 extractNames( matches
),
543 testCase
.expectedMatches
,
548 matches
= model
.findMatches( 'foo' );
550 $.isEmptyObject( matches
),
551 'findMatches returns an empty object when no results found'
555 QUnit
.test( 'getParametersFromFilters', function ( assert
) {
556 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
558 model
.initializeFilters( filterDefinition
, viewsDefinition
);
560 // Starting with all filters unselected
562 model
.getParametersFromFilters(),
563 baseParamRepresentation
,
564 'Unselected filters return all parameters falsey or \'\'.'
568 model
.toggleFiltersSelected( {
569 group1__filter1
: true
571 // Only one filter in one group
573 model
.getParametersFromFilters(),
574 $.extend( true, {}, baseParamRepresentation
, {
575 // Group 1 (one selected, the others are true)
579 'One filter in one "send_unselected_if_any" group returns the other parameters truthy.'
583 model
.toggleFiltersSelected( {
584 group1__filter1
: true,
585 group1__filter2
: true
587 // Two selected filters in one group
589 model
.getParametersFromFilters(),
590 $.extend( true, {}, baseParamRepresentation
, {
591 // Group 1 (two selected, the other is true)
594 'Two filters in one "send_unselected_if_any" group returns the other parameters truthy.'
598 model
.toggleFiltersSelected( {
599 group1__filter1
: true,
600 group1__filter2
: true,
601 group1__filter3
: true
603 // All filters of the group are selected == this is the same as not selecting any
605 model
.getParametersFromFilters(),
606 baseParamRepresentation
,
607 'All filters selected in one "send_unselected_if_any" group returns all parameters falsy.'
610 // Select 1 filter from string_options
611 model
.toggleFiltersSelected( {
612 group3__filter7
: true,
613 group3__filter8
: false,
614 group3__filter9
: false
616 // All filters of the group are selected == this is the same as not selecting any
618 model
.getParametersFromFilters(),
619 $.extend( true, {}, baseParamRepresentation
, {
622 'One filter selected in "string_option" group returns that filter in the value.'
625 // Select 2 filters from string_options
626 model
.toggleFiltersSelected( {
627 group3__filter7
: true,
628 group3__filter8
: true,
629 group3__filter9
: false
631 // All filters of the group are selected == this is the same as not selecting any
633 model
.getParametersFromFilters(),
634 $.extend( true, {}, baseParamRepresentation
, {
635 group3
: 'filter7,filter8'
637 'Two filters selected in "string_option" group returns those filters in the value.'
640 // Select 3 filters from string_options
641 model
.toggleFiltersSelected( {
642 group3__filter7
: true,
643 group3__filter8
: true,
644 group3__filter9
: true
646 // All filters of the group are selected == this is the same as not selecting any
648 model
.getParametersFromFilters(),
649 $.extend( true, {}, baseParamRepresentation
, {
652 'All filters selected in "string_option" group returns \'all\'.'
656 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
657 model
.initializeFilters( filterDefinition
, viewsDefinition
);
659 // Select an option from single_option group
660 model
.toggleFiltersSelected( {
661 group4__option2
: true
663 // All filters of the group are selected == this is the same as not selecting any
665 model
.getParametersFromFilters(),
666 $.extend( true, {}, baseParamRepresentation
, {
669 'Selecting an option from "single_option" group returns that option as a value.'
672 // Select a different option from single_option group
673 model
.toggleFiltersSelected( {
674 group4__option3
: true
676 // All filters of the group are selected == this is the same as not selecting any
678 model
.getParametersFromFilters(),
679 $.extend( true, {}, baseParamRepresentation
, {
682 'Selecting a different option from "single_option" group changes the selection.'
686 QUnit
.test( 'getParametersFromFilters (custom object)', function ( assert
) {
687 // This entire test uses different base definition than the global one
688 // on purpose, to verify that the values inserted as a custom object
689 // are the ones we expect in return
691 model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
695 type
: 'send_unselected_if_any',
697 { name
: 'hidefilter1', label
: 'Hide filter 1', description
: '' },
698 { name
: 'hidefilter2', label
: 'Hide filter 2', description
: '' },
699 { name
: 'hidefilter3', label
: 'Hide filter 3', description
: '' }
704 type
: 'send_unselected_if_any',
706 { name
: 'hidefilter4', label
: 'Hide filter 4', description
: '' },
707 { name
: 'hidefilter5', label
: 'Hide filter 5', description
: '' },
708 { name
: 'hidefilter6', label
: 'Hide filter 6', description
: '' }
713 type
: 'string_options',
716 { name
: 'filter7', label
: 'Hide filter 7', description
: '' },
717 { name
: 'filter8', label
: 'Hide filter 8', description
: '' },
718 { name
: 'filter9', label
: 'Hide filter 9', description
: '' }
723 type
: 'single_option',
725 { name
: 'filter10', label
: 'Hide filter 10', description
: '' },
726 { name
: 'filter11', label
: 'Hide filter 11', description
: '' },
727 { name
: 'filter12', label
: 'Hide filter 12', description
: '' }
742 // This is mocking the cases above, both
743 // - 'Two filters in one "send_unselected_if_any" group returns the other parameters truthy.'
744 // - 'Two filters selected in "string_option" group returns those filters in the value.'
746 group1__hidefilter1
: true,
747 group1__hidefilter2
: true,
748 group1__hidefilter3
: false,
749 group2__hidefilter4
: false,
750 group2__hidefilter5
: false,
751 group2__hidefilter6
: false,
752 group3__filter7
: true,
753 group3__filter8
: true,
754 group3__filter9
: false
756 expected
: $.extend( true, {}, baseResult
, {
757 // Group 1 (two selected, the others are true)
759 // Group 3 (two selected)
760 group3
: 'filter7,filter8'
762 msg
: 'Given an explicit (complete) filter state object, the result is the same as if the object given represented the model state.'
765 // This is mocking case above
766 // - 'One filter in one "send_unselected_if_any" group returns the other parameters truthy.'
768 group1__hidefilter1
: 1
770 expected
: $.extend( true, {}, baseResult
, {
771 // Group 1 (one selected, the others are true)
775 msg
: 'Given an explicit (incomplete) filter state object, the result is the same as if the object give represented the model state.'
779 group4__filter10
: true
781 expected
: $.extend( true, {}, baseResult
, {
784 msg
: 'Given a single value for "single_option" that option is represented in the result.'
788 group4__filter10
: true,
789 group4__filter11
: true
791 expected
: $.extend( true, {}, baseResult
, {
794 msg
: 'Given more than one true value for "single_option" (which should not happen!) only the first value counts, and the second is ignored.'
798 expected
: baseResult
,
799 msg
: 'Given an explicit empty object, the result is all filters set to their falsey unselected value.'
803 model
.initializeFilters( definition
);
804 // Store original state
805 originalState
= model
.getSelectedState();
808 cases
.forEach( function ( test
) {
810 model
.getParametersFromFilters( test
.input
),
816 // After doing the above tests, make sure the actual state
817 // of the filter stayed the same
819 model
.getSelectedState(),
821 'Running the method with external definition to parse does not actually change the state of the model'
825 QUnit
.test( 'getFiltersFromParameters', function ( assert
) {
826 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
828 model
.initializeFilters( filterDefinition
, viewsDefinition
);
830 // Empty query = only default values
832 model
.getFiltersFromParameters( {} ),
833 baseFilterRepresentation
,
834 'Empty parameter query results in an object representing all filters set to their base state'
838 model
.getFiltersFromParameters( {
841 $.extend( {}, baseFilterRepresentation
, {
842 group1__filter1
: true, // The text is "show filter 1"
843 group1__filter2
: false, // The text is "show filter 2"
844 group1__filter3
: true // The text is "show filter 3"
846 'One truthy parameter in a group whose other parameters are true by default makes the rest of the filters in the group false (unchecked)'
850 model
.getFiltersFromParameters( {
855 $.extend( {}, baseFilterRepresentation
, {
856 group1__filter1
: false, // The text is "show filter 1"
857 group1__filter2
: false, // The text is "show filter 2"
858 group1__filter3
: false // The text is "show filter 3"
860 'All paremeters in the same \'send_unselected_if_any\' group false is equivalent to none are truthy (checked) in the interface'
863 // The ones above don't update the model, so we have a clean state.
864 // getFiltersFromParameters is stateless; any change is unaffected by the current state
865 // This test is demonstrating wrong usage of the method;
866 // We should be aware that getFiltersFromParameters is stateless,
867 // so each call gives us a filter state that only reflects the query given.
868 // This means that the two calls to toggleFiltersSelected() below collide.
869 // The result of the first is overridden by the result of the second,
870 // since both get a full state object from getFiltersFromParameters that **only** relates
871 // to the input it receives.
872 model
.toggleFiltersSelected(
873 model
.getFiltersFromParameters( {
878 model
.toggleFiltersSelected(
879 model
.getFiltersFromParameters( {
884 // The result here is ignoring the first toggleFiltersSelected call
886 model
.getSelectedState(),
887 $.extend( {}, baseFilterRepresentation
, {
888 group2__filter4
: true,
889 group2__filter5
: true,
890 group2__filter6
: false
892 'getFiltersFromParameters does not care about previous or existing state.'
896 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
897 model
.initializeFilters( filterDefinition
, viewsDefinition
);
899 model
.toggleFiltersSelected(
900 model
.getFiltersFromParameters( {
905 model
.getSelectedState(),
906 $.extend( {}, baseFilterRepresentation
, {
907 group3__filter7
: true,
908 group3__filter8
: false,
909 group3__filter9
: false
911 'A \'string_options\' parameter containing 1 value, results in the corresponding filter as checked'
914 model
.toggleFiltersSelected(
915 model
.getFiltersFromParameters( {
916 group3
: 'filter7,filter8'
920 model
.getSelectedState(),
921 $.extend( {}, baseFilterRepresentation
, {
922 group3__filter7
: true,
923 group3__filter8
: true,
924 group3__filter9
: false
926 'A \'string_options\' parameter containing 2 values, results in both corresponding filters as checked'
929 model
.toggleFiltersSelected(
930 model
.getFiltersFromParameters( {
931 group3
: 'filter7,filter8,filter9'
935 model
.getSelectedState(),
936 $.extend( {}, baseFilterRepresentation
, {
937 group3__filter7
: true,
938 group3__filter8
: true,
939 group3__filter9
: true
941 'A \'string_options\' parameter containing all values, results in all filters of the group as checked.'
944 model
.toggleFiltersSelected(
945 model
.getFiltersFromParameters( {
946 group3
: 'filter7,all,filter9'
950 model
.getSelectedState(),
951 $.extend( {}, baseFilterRepresentation
, {
952 group3__filter7
: true,
953 group3__filter8
: true,
954 group3__filter9
: true
956 'A \'string_options\' parameter containing the value \'all\', results in all filters of the group as checked.'
959 model
.toggleFiltersSelected(
960 model
.getFiltersFromParameters( {
961 group3
: 'filter7,foo,filter9'
965 model
.getSelectedState(),
966 $.extend( {}, baseFilterRepresentation
, {
967 group3__filter7
: true,
968 group3__filter8
: false,
969 group3__filter9
: true
971 'A \'string_options\' parameter containing an invalid value, results in the invalid value ignored and the valid corresponding filters checked.'
974 model
.toggleFiltersSelected(
975 model
.getFiltersFromParameters( {
980 model
.getSelectedState(),
981 $.extend( {}, baseFilterRepresentation
, {
982 group4__option1
: true,
983 group4__option2
: false
985 'A \'single_option\' parameter reflects a single selected value.'
989 model
.getFiltersFromParameters( {
990 group4
: 'option1,option2'
992 baseFilterRepresentation
,
993 'An invalid \'single_option\' parameter is ignored.'
996 // Change to one value
997 model
.toggleFiltersSelected(
998 model
.getFiltersFromParameters( {
1002 // Change again to another value
1003 model
.toggleFiltersSelected(
1004 model
.getFiltersFromParameters( {
1009 model
.getSelectedState(),
1010 $.extend( {}, baseFilterRepresentation
, {
1011 group4__option2
: true
1013 'A \'single_option\' parameter always reflects the latest selected value.'
1017 QUnit
.test( 'sanitizeStringOptionGroup', function ( assert
) {
1018 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1020 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1023 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'filter1', 'filter2' ] ),
1024 [ 'filter1', 'filter2' ],
1025 'Remove duplicate values'
1029 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'foo', 'filter2' ] ),
1030 [ 'filter1', 'filter2' ],
1031 'Remove invalid values'
1035 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'all', 'filter2' ] ),
1037 'If any value is "all", the only value is "all".'
1041 QUnit
.test( 'Filter interaction: subsets', function ( assert
) {
1042 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1044 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1046 // Select a filter that has subset with another filter
1047 model
.toggleFiltersSelected( {
1048 group1__filter1
: true
1051 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
1053 model
.getFullState(),
1054 $.extend( true, {}, baseFullFilterState
, {
1055 group1__filter1
: { selected
: true },
1056 group1__filter2
: { included
: true },
1057 group1__filter3
: { included
: true },
1058 // Conflicts are affected
1059 group2__filter4
: { conflicted
: true },
1060 group2__filter5
: { conflicted
: true },
1061 group2__filter6
: { conflicted
: true }
1063 'Filters with subsets are represented in the model.'
1066 // Select another filter that has a subset with the same previous filter
1067 model
.toggleFiltersSelected( {
1068 group1__filter2
: true
1070 model
.reassessFilterInteractions( model
.getItemByName( 'filter2' ) );
1072 model
.getFullState(),
1073 $.extend( true, {}, baseFullFilterState
, {
1074 group1__filter1
: { selected
: true },
1075 group1__filter2
: { selected
: true, included
: true },
1076 group1__filter3
: { included
: true },
1077 // Conflicts are affected
1078 group2__filter6
: { conflicted
: true }
1080 'Filters that have multiple subsets are represented.'
1083 // Remove one filter (but leave the other) that affects filter3
1084 model
.toggleFiltersSelected( {
1085 group1__filter1
: false
1087 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
1089 model
.getFullState(),
1090 $.extend( true, {}, baseFullFilterState
, {
1091 group1__filter2
: { selected
: true, included
: false },
1092 group1__filter3
: { included
: true },
1093 // Conflicts are affected
1094 group2__filter6
: { conflicted
: true }
1096 'Removing a filter only un-includes its subset if there is no other filter affecting.'
1099 model
.toggleFiltersSelected( {
1100 group1__filter2
: false
1102 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1104 model
.getFullState(),
1105 baseFullFilterState
,
1106 'Removing all supersets also un-includes the subsets.'
1110 QUnit
.test( 'Filter interaction: full coverage', function ( assert
) {
1111 var model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
1112 isCapsuleItemMuted = function ( filterName
) {
1113 var itemModel
= model
.getItemByName( filterName
),
1114 groupModel
= itemModel
.getGroupModel();
1116 // This is the logic inside the capsule widget
1118 // The capsule item widget only appears if the item is selected
1119 itemModel
.isSelected() &&
1120 // Muted state is only valid if group is full coverage and all items are selected
1121 groupModel
.isFullCoverage() && groupModel
.areAllSelected()
1124 getCurrentItemsMutedState = function () {
1126 group1__filter1
: isCapsuleItemMuted( 'group1__filter1' ),
1127 group1__filter2
: isCapsuleItemMuted( 'group1__filter2' ),
1128 group1__filter3
: isCapsuleItemMuted( 'group1__filter3' ),
1129 group2__filter4
: isCapsuleItemMuted( 'group2__filter4' ),
1130 group2__filter5
: isCapsuleItemMuted( 'group2__filter5' ),
1131 group2__filter6
: isCapsuleItemMuted( 'group2__filter6' )
1135 group1__filter1
: false,
1136 group1__filter2
: false,
1137 group1__filter3
: false,
1138 group2__filter4
: false,
1139 group2__filter5
: false,
1140 group2__filter6
: false
1143 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1145 // Starting state, no selection, all items are non-muted
1147 getCurrentItemsMutedState(),
1149 'No selection - all items are non-muted'
1152 // Select most (but not all) items in each group
1153 model
.toggleFiltersSelected( {
1154 group1__filter1
: true,
1155 group1__filter2
: true,
1156 group2__filter4
: true,
1157 group2__filter5
: true
1160 // Both groups have multiple (but not all) items selected, all items are non-muted
1162 getCurrentItemsMutedState(),
1164 'Not all items in the group selected - all items are non-muted'
1167 // Select all items in 'fullCoverage' group (group2)
1168 model
.toggleFiltersSelected( {
1169 group2__filter6
: true
1172 // Group2 (full coverage) has all items selected, all its items are muted
1174 getCurrentItemsMutedState(),
1175 $.extend( {}, baseMuteState
, {
1176 group2__filter4
: true,
1177 group2__filter5
: true,
1178 group2__filter6
: true
1180 'All items in \'full coverage\' group are selected - all items in the group are muted'
1183 // Select all items in non 'fullCoverage' group (group1)
1184 model
.toggleFiltersSelected( {
1185 group1__filter3
: true
1188 // Group1 (full coverage) has all items selected, no items in it are muted (non full coverage)
1190 getCurrentItemsMutedState(),
1191 $.extend( {}, baseMuteState
, {
1192 group2__filter4
: true,
1193 group2__filter5
: true,
1194 group2__filter6
: true
1196 'All items in a non \'full coverage\' group are selected - none of the items in the group are muted'
1199 // Uncheck an item from each group
1200 model
.toggleFiltersSelected( {
1201 group1__filter3
: false,
1202 group2__filter5
: false
1205 getCurrentItemsMutedState(),
1207 'Not all items in the group are checked - all items are non-muted regardless of group coverage'
1211 QUnit
.test( 'Filter interaction: conflicts', function ( assert
) {
1212 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1214 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1217 model
.getFullState(),
1218 baseFullFilterState
,
1219 'Initial state: no conflicts because no selections.'
1222 // Select a filter that has a conflict with an entire group
1223 model
.toggleFiltersSelected( {
1224 group1__filter1
: true // conflicts: entire of group 2 ( filter4, filter5, filter6)
1227 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
1230 model
.getFullState(),
1231 $.extend( true, {}, baseFullFilterState
, {
1232 group1__filter1
: { selected
: true },
1233 group2__filter4
: { conflicted
: true },
1234 group2__filter5
: { conflicted
: true },
1235 group2__filter6
: { conflicted
: true },
1236 // Subsets are affected by the selection
1237 group1__filter2
: { included
: true },
1238 group1__filter3
: { included
: true }
1240 'Selecting a filter that conflicts with a group sets all the conflicted group items as "conflicted".'
1243 // Select one of the conflicts (both filters are now conflicted and selected)
1244 model
.toggleFiltersSelected( {
1245 group2__filter4
: true // conflicts: filter 1
1247 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter4' ) );
1250 model
.getFullState(),
1251 $.extend( true, {}, baseFullFilterState
, {
1252 group1__filter1
: { selected
: true, conflicted
: true },
1253 group2__filter4
: { selected
: true, conflicted
: true },
1254 group2__filter5
: { conflicted
: true },
1255 group2__filter6
: { conflicted
: true },
1256 // Subsets are affected by the selection
1257 group1__filter2
: { included
: true },
1258 group1__filter3
: { included
: true }
1260 'Selecting a conflicting filter inside a group, sets both sides to conflicted and selected.'
1264 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1265 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1267 // Select a filter that has a conflict with a specific filter
1268 model
.toggleFiltersSelected( {
1269 group1__filter2
: true // conflicts: filter6
1271 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1274 model
.getFullState(),
1275 $.extend( true, {}, baseFullFilterState
, {
1276 group1__filter2
: { selected
: true },
1277 group2__filter6
: { conflicted
: true },
1278 // Subsets are affected by the selection
1279 group1__filter3
: { included
: true }
1281 'Selecting a filter that conflicts with another filter sets the other as "conflicted".'
1284 // Select the conflicting filter
1285 model
.toggleFiltersSelected( {
1286 group2__filter6
: true // conflicts: filter2
1289 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter6' ) );
1292 model
.getFullState(),
1293 $.extend( true, {}, baseFullFilterState
, {
1294 group1__filter2
: { selected
: true, conflicted
: true },
1295 group2__filter6
: { selected
: true, conflicted
: true },
1296 // This is added to the conflicts because filter6 is part of group2,
1297 // who is in conflict with filter1; note that filter2 also conflicts
1298 // with filter6 which means that filter1 conflicts with filter6 (because it's in group2)
1299 // and also because its **own sibling** (filter2) is **also** in conflict with the
1300 // selected items in group2 (filter6)
1301 group1__filter1
: { conflicted
: true },
1303 // Subsets are affected by the selection
1304 group1__filter3
: { included
: true }
1306 'Selecting a conflicting filter with an individual filter, sets both sides to conflicted and selected.'
1309 // Now choose a non-conflicting filter from the group
1310 model
.toggleFiltersSelected( {
1311 group2__filter5
: true
1314 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter5' ) );
1317 model
.getFullState(),
1318 $.extend( true, {}, baseFullFilterState
, {
1319 group1__filter2
: { selected
: true },
1320 group2__filter6
: { selected
: true },
1321 group2__filter5
: { selected
: true },
1322 // Filter6 and filter1 are no longer in conflict because
1323 // filter5, while it is in conflict with filter1, it is
1324 // not in conflict with filter2 - and since filter2 is
1325 // selected, it removes the conflict bidirectionally
1327 // Subsets are affected by the selection
1328 group1__filter3
: { included
: true }
1330 'Selecting a non-conflicting filter within the group of a conflicting filter removes the conflicts.'
1333 // Followup on the previous test, unselect filter2 so filter1
1334 // is now the only one selected in its own group, and since
1335 // it is in conflict with the entire of group2, it means
1336 // filter1 is once again conflicted
1337 model
.toggleFiltersSelected( {
1338 group1__filter2
: false
1341 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1344 model
.getFullState(),
1345 $.extend( true, {}, baseFullFilterState
, {
1346 group1__filter1
: { conflicted
: true },
1347 group2__filter6
: { selected
: true },
1348 group2__filter5
: { selected
: true }
1350 'Unselecting an item that did not conflict returns the conflict state.'
1353 // Followup #2: Now actually select filter1, and make everything conflicted
1354 model
.toggleFiltersSelected( {
1355 group1__filter1
: true
1358 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
1361 model
.getFullState(),
1362 $.extend( true, {}, baseFullFilterState
, {
1363 group1__filter1
: { selected
: true, conflicted
: true },
1364 group2__filter6
: { selected
: true, conflicted
: true },
1365 group2__filter5
: { selected
: true, conflicted
: true },
1366 group2__filter4
: { conflicted
: true }, // Not selected but conflicted because it's in group2
1367 // Subsets are affected by the selection
1368 group1__filter2
: { included
: true },
1369 group1__filter3
: { included
: true }
1371 'Selecting an item that conflicts with a whole group makes all selections in that group conflicted.'
1376 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1377 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1379 // Select a filter that has a conflict with a specific filter
1380 model
.toggleFiltersSelected( {
1381 group1__filter2
: true // conflicts: filter6
1384 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1387 model
.getFullState(),
1388 $.extend( true, {}, baseFullFilterState
, {
1389 group1__filter2
: { selected
: true },
1390 group2__filter6
: { conflicted
: true },
1391 // Subsets are affected by the selection
1392 group1__filter3
: { included
: true }
1394 'Simple case: Selecting a filter that conflicts with another filter sets the other as "conflicted".'
1397 model
.toggleFiltersSelected( {
1398 group1__filter3
: true // conflicts: filter6
1401 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter3' ) );
1404 model
.getFullState(),
1405 $.extend( true, {}, baseFullFilterState
, {
1406 group1__filter2
: { selected
: true },
1407 // Subsets are affected by the selection
1408 group1__filter3
: { selected
: true, included
: true }
1410 'Simple case: Selecting a filter that is not in conflict removes the conflict.'
1414 QUnit
.test( 'Filter highlights', function ( assert
) {
1415 // We are using a different (smaller) definition here than the global one
1416 var definition
= [ {
1419 type
: 'string_options',
1421 { name
: 'filter1', cssClass
: 'class1', label
: '1', description
: '1' },
1422 { name
: 'filter2', cssClass
: 'class2', label
: '2', description
: '2' },
1423 { name
: 'filter3', cssClass
: 'class3', label
: '3', description
: '3' },
1424 { name
: 'filter4', cssClass
: 'class4', label
: '4', description
: '4' },
1425 { name
: 'filter5', cssClass
: 'class5', label
: '5', description
: '5' },
1426 { name
: 'filter6', label
: '6', description
: '6' }
1429 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1431 model
.initializeFilters( definition
);
1434 !model
.isHighlightEnabled(),
1435 'Initially, highlight is disabled.'
1438 model
.toggleHighlight( true );
1440 model
.isHighlightEnabled(),
1441 'Highlight is enabled on toggle.'
1444 model
.setHighlightColor( 'group1__filter1', 'color1' );
1445 model
.setHighlightColor( 'group1__filter2', 'color2' );
1448 model
.getHighlightedItems().map( function ( item
) {
1449 return item
.getName();
1455 'Highlighted items are highlighted.'
1459 model
.getItemByName( 'group1__filter1' ).getHighlightColor(),
1461 'Item highlight color is set.'
1464 model
.setHighlightColor( 'group1__filter1', 'color1changed' );
1466 model
.getItemByName( 'group1__filter1' ).getHighlightColor(),
1468 'Item highlight color is changed on setHighlightColor.'
1471 model
.clearHighlightColor( 'group1__filter1' );
1473 model
.getHighlightedItems().map( function ( item
) {
1474 return item
.getName();
1479 'Clear highlight from an item results in the item no longer being highlighted.'
1483 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1484 model
.initializeFilters( definition
);
1486 model
.setHighlightColor( 'group1__filter1', 'color1' );
1487 model
.setHighlightColor( 'group1__filter2', 'color2' );
1488 model
.setHighlightColor( 'group1__filter3', 'color3' );
1491 model
.getHighlightedItems().map( function ( item
) {
1492 return item
.getName();
1499 'Even if highlights are not enabled, the items remember their highlight state'
1500 // NOTE: When actually displaying the highlights, the UI checks whether
1501 // highlighting is generally active and then goes over the highlighted
1502 // items. The item models, however, and the view model in general, still
1503 // retains the knowledge about which filters have different colors, so we
1504 // can seamlessly return to the colors the user previously chose if they
1505 // reapply highlights.
1509 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1510 model
.initializeFilters( definition
);
1512 model
.setHighlightColor( 'group1__filter1', 'color1' );
1513 model
.setHighlightColor( 'group1__filter6', 'color6' );
1516 model
.getHighlightedItems().map( function ( item
) {
1517 return item
.getName();
1522 'Items without a specified class identifier are not highlighted.'
1525 }( mediaWiki
, jQuery
) );