1 /* eslint-disable camelcase */
3 var filterDefinition
= [ {
5 type
: 'send_unselected_if_any',
8 name
: 'filter1', label
: 'group1filter1-label', description
: 'group1filter1-desc',
10 conflicts
: [ { group
: 'group2' } ],
23 name
: 'filter2', label
: 'group1filter2-label', description
: 'group1filter2-desc',
24 conflicts
: [ { group
: 'group2', filter
: 'filter6' } ],
32 { name
: 'filter3', label
: 'group1filter3-label', description
: 'group1filter3-desc', default: true }
36 type
: 'send_unselected_if_any',
38 conflicts
: [ { group
: 'group1', filter
: 'filter1' } ],
40 { name
: 'filter4', label
: 'group2filter4-label', description
: 'group2filter4-desc' },
41 { name
: 'filter5', label
: 'group2filter5-label', description
: 'group2filter5-desc', default: true },
43 name
: 'filter6', label
: 'group2filter6-label', description
: 'group2filter6-desc',
44 conflicts
: [ { group
: 'group1', filter
: 'filter2' } ]
49 type
: 'string_options',
53 { name
: 'filter7', label
: 'group3filter7-label', description
: 'group3filter7-desc' },
54 { name
: 'filter8', label
: 'group3filter8-label', description
: 'group3filter8-desc' },
55 { name
: 'filter9', label
: 'group3filter9-label', description
: 'group3filter9-desc' }
59 type
: 'single_option',
62 { name
: 'option1', label
: 'group4option1-label', description
: 'group4option1-desc' },
63 { name
: 'option2', label
: 'group4option2-label', description
: 'group4option2-desc' },
64 { name
: 'option3', label
: 'group4option3-label', description
: 'group4option3-desc' }
68 type
: 'single_option',
70 { name
: 'option1', label
: 'group5option1-label', description
: 'group5option1-desc' },
71 { name
: 'option2', label
: 'group5option2-label', description
: 'group5option2-desc' },
72 { name
: 'option3', label
: 'group5option3-label', description
: 'group5option3-desc' }
78 { name
: 'group6option1', label
: 'group6option1-label', description
: 'group5option1-desc' },
79 { name
: 'group6option2', label
: 'group6option2-label', description
: 'group5option2-desc', default: true, useDefaultAsBaseValue
: true },
80 { name
: 'group6option3', label
: 'group6option3-label', description
: 'group5option3-desc', default: true }
90 type
: 'string_options',
93 { name
: 0, label
: 'Main' },
94 { name
: 1, label
: 'Talk' },
95 { name
: 2, label
: 'User' },
96 { name
: 3, label
: 'User talk' }
101 defaultParameters
= {
116 baseParamRepresentation
= {
131 baseFilterRepresentation
= {
132 group1__filter1
: false,
133 group1__filter2
: false,
134 group1__filter3
: false,
135 group2__filter4
: false,
136 group2__filter5
: false,
137 group2__filter6
: false,
138 group3__filter7
: false,
139 group3__filter8
: false,
140 group3__filter9
: false,
141 // The 'single_value' type of group can't have empty value; it's either
142 // the default given or the first item that will get the truthy value
143 group4__option1
: false,
144 group4__option2
: true, // Default
145 group4__option3
: false,
146 group5__option1
: true, // No default set, first item is default value
147 group5__option2
: false,
148 group5__option3
: false,
149 group6__group6option1
: false,
150 group6__group6option2
: true,
151 group6__group6option3
: false,
157 baseFullFilterState
= {
158 group1__filter1
: { selected
: false, conflicted
: false, included
: false },
159 group1__filter2
: { selected
: false, conflicted
: false, included
: false },
160 group1__filter3
: { selected
: false, conflicted
: false, included
: false },
161 group2__filter4
: { selected
: false, conflicted
: false, included
: false },
162 group2__filter5
: { selected
: false, conflicted
: false, included
: false },
163 group2__filter6
: { selected
: false, conflicted
: false, included
: false },
164 group3__filter7
: { selected
: false, conflicted
: false, included
: false },
165 group3__filter8
: { selected
: false, conflicted
: false, included
: false },
166 group3__filter9
: { selected
: false, conflicted
: false, included
: false },
167 group4__option1
: { selected
: false, conflicted
: false, included
: false },
168 group4__option2
: { selected
: true, conflicted
: false, included
: false },
169 group4__option3
: { selected
: false, conflicted
: false, included
: false },
170 group5__option1
: { selected
: true, conflicted
: false, included
: false },
171 group5__option2
: { selected
: false, conflicted
: false, included
: false },
172 group5__option3
: { selected
: false, conflicted
: false, included
: false },
173 group6__group6option1
: { selected
: false, conflicted
: false, included
: false },
174 group6__group6option2
: { selected
: true, conflicted
: false, included
: false },
175 group6__group6option3
: { selected
: false, conflicted
: false, included
: false },
176 namespace__0
: { selected
: false, conflicted
: false, included
: false },
177 namespace__1
: { selected
: false, conflicted
: false, included
: false },
178 namespace__2
: { selected
: false, conflicted
: false, included
: false },
179 namespace__3
: { selected
: false, conflicted
: false, included
: false }
182 QUnit
.module( 'mediawiki.rcfilters - FiltersViewModel', QUnit
.newMwEnvironment( {
184 'group1filter1-label': 'Group 1: Filter 1 title',
185 'group1filter1-desc': 'Description of Filter 1 in Group 1',
186 'group1filter2-label': 'Group 1: Filter 2 title',
187 'group1filter2-desc': 'Description of Filter 2 in Group 1',
188 'group1filter3-label': 'Group 1: Filter 3',
189 'group1filter3-desc': 'Description of Filter 3 in Group 1',
191 'group2filter4-label': 'Group 2: Filter 4 title',
192 'group2filter4-desc': 'Description of Filter 4 in Group 2',
193 'group2filter5-label': 'Group 2: Filter 5',
194 'group2filter5-desc': 'Description of Filter 5 in Group 2',
195 'group2filter6-label': 'xGroup 2: Filter 6',
196 'group2filter6-desc': 'Description of Filter 6 in Group 2'
199 wgStructuredChangeFiltersEnableExperimentalViews
: true
203 QUnit
.test( 'Setting up filters', function ( assert
) {
204 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
206 model
.initializeFilters( filterDefinition
, viewsDefinition
);
208 // Test that all items were created
210 Object
.keys( baseFilterRepresentation
).every( function ( filterName
) {
211 return model
.getItemByName( filterName
) instanceof mw
.rcfilters
.dm
.FilterItem
;
213 'Filters instantiated and stored correctly'
217 model
.getSelectedState(),
218 baseFilterRepresentation
,
219 'Initial state of filters'
222 model
.toggleFiltersSelected( {
223 group1__filter1
: true,
224 group2__filter5
: true,
225 group3__filter7
: true
228 model
.getSelectedState(),
229 $.extend( true, {}, baseFilterRepresentation
, {
230 group1__filter1
: true,
231 group2__filter5
: true,
232 group3__filter7
: true
234 'Updating filter states correctly'
238 QUnit
.test( 'Default filters', function ( assert
) {
239 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
241 model
.initializeFilters( filterDefinition
, viewsDefinition
);
243 // Empty query = only default values
245 model
.getDefaultParams(),
247 'Default parameters are stored properly per filter and group'
251 QUnit
.test( 'Finding matching filters', function ( assert
) {
257 group1
: [ 'group1__filter1', 'group1__filter2', 'group1__filter3' ],
258 group2
: [ 'group2__filter4', 'group2__filter5' ]
260 reason
: 'Finds filters starting with the query string'
265 group2
: [ 'group2__filter4', 'group2__filter5', 'group2__filter6' ]
267 reason
: 'Finds filters containing the query string in their description'
272 group1
: [ 'group1__filter1', 'group1__filter2' ],
273 group2
: [ 'group2__filter4' ]
275 reason
: 'Finds filters containing the query string in their group title'
280 namespace: [ 'namespace__0' ]
282 reason
: 'Finds item in view when a prefix is used'
287 reason
: 'Finds no results if using namespaces prefix (:) to search for filter title'
290 model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
291 extractNames = function ( matches
) {
293 Object
.keys( matches
).forEach( function ( groupName
) {
294 result
[ groupName
] = matches
[ groupName
].map( function ( item
) {
295 return item
.getName();
301 model
.initializeFilters( filterDefinition
, viewsDefinition
);
303 testCases
.forEach( function ( testCase
) {
304 matches
= model
.findMatches( testCase
.query
);
306 extractNames( matches
),
307 testCase
.expectedMatches
,
312 matches
= model
.findMatches( 'foo' );
314 $.isEmptyObject( matches
),
315 'findMatches returns an empty object when no results found'
319 QUnit
.test( 'getParametersFromFilters', function ( assert
) {
320 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
322 model
.initializeFilters( filterDefinition
, viewsDefinition
);
324 // Starting with all filters unselected
326 model
.getParametersFromFilters(),
327 baseParamRepresentation
,
328 'Unselected filters return all parameters falsey or \'\'.'
332 model
.toggleFiltersSelected( {
333 group1__filter1
: true
335 // Only one filter in one group
337 model
.getParametersFromFilters(),
338 $.extend( true, {}, baseParamRepresentation
, {
339 // Group 1 (one selected, the others are true)
343 'One filter in one "send_unselected_if_any" group returns the other parameters truthy.'
347 model
.toggleFiltersSelected( {
348 group1__filter1
: true,
349 group1__filter2
: true
351 // Two selected filters in one group
353 model
.getParametersFromFilters(),
354 $.extend( true, {}, baseParamRepresentation
, {
355 // Group 1 (two selected, the other is true)
358 'Two filters in one "send_unselected_if_any" group returns the other parameters truthy.'
362 model
.toggleFiltersSelected( {
363 group1__filter1
: true,
364 group1__filter2
: true,
365 group1__filter3
: true
367 // All filters of the group are selected == this is the same as not selecting any
369 model
.getParametersFromFilters(),
370 baseParamRepresentation
,
371 'All filters selected in one "send_unselected_if_any" group returns all parameters falsy.'
374 // Select 1 filter from string_options
375 model
.toggleFiltersSelected( {
376 group3__filter7
: true,
377 group3__filter8
: false,
378 group3__filter9
: false
380 // All filters of the group are selected == this is the same as not selecting any
382 model
.getParametersFromFilters(),
383 $.extend( true, {}, baseParamRepresentation
, {
386 'One filter selected in "string_option" group returns that filter in the value.'
389 // Select 2 filters from string_options
390 model
.toggleFiltersSelected( {
391 group3__filter7
: true,
392 group3__filter8
: true,
393 group3__filter9
: false
395 // All filters of the group are selected == this is the same as not selecting any
397 model
.getParametersFromFilters(),
398 $.extend( true, {}, baseParamRepresentation
, {
399 group3
: 'filter7,filter8'
401 'Two filters selected in "string_option" group returns those filters in the value.'
404 // Select 3 filters from string_options
405 model
.toggleFiltersSelected( {
406 group3__filter7
: true,
407 group3__filter8
: true,
408 group3__filter9
: true
410 // All filters of the group are selected == this is the same as not selecting any
412 model
.getParametersFromFilters(),
413 $.extend( true, {}, baseParamRepresentation
, {
416 'All filters selected in "string_option" group returns \'all\'.'
420 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
421 model
.initializeFilters( filterDefinition
, viewsDefinition
);
423 // Select an option from single_option group
424 model
.toggleFiltersSelected( {
425 group4__option2
: true
427 // All filters of the group are selected == this is the same as not selecting any
429 model
.getParametersFromFilters(),
430 $.extend( true, {}, baseParamRepresentation
, {
433 'Selecting an option from "single_option" group returns that option as a value.'
436 // Select a different option from single_option group
437 model
.toggleFiltersSelected( {
438 group4__option3
: true
440 // All filters of the group are selected == this is the same as not selecting any
442 model
.getParametersFromFilters(),
443 $.extend( true, {}, baseParamRepresentation
, {
446 'Selecting a different option from "single_option" group changes the selection.'
450 QUnit
.test( 'getParametersFromFilters (custom object)', function ( assert
) {
451 // This entire test uses different base definition than the global one
452 // on purpose, to verify that the values inserted as a custom object
453 // are the ones we expect in return
455 model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
459 type
: 'send_unselected_if_any',
461 { name
: 'hidefilter1', label
: 'Hide filter 1', description
: '' },
462 { name
: 'hidefilter2', label
: 'Hide filter 2', description
: '' },
463 { name
: 'hidefilter3', label
: 'Hide filter 3', description
: '' }
468 type
: 'send_unselected_if_any',
470 { name
: 'hidefilter4', label
: 'Hide filter 4', description
: '' },
471 { name
: 'hidefilter5', label
: 'Hide filter 5', description
: '' },
472 { name
: 'hidefilter6', label
: 'Hide filter 6', description
: '' }
477 type
: 'string_options',
480 { name
: 'filter7', label
: 'Hide filter 7', description
: '' },
481 { name
: 'filter8', label
: 'Hide filter 8', description
: '' },
482 { name
: 'filter9', label
: 'Hide filter 9', description
: '' }
487 type
: 'single_option',
489 { name
: 'filter10', label
: 'Hide filter 10', description
: '' },
490 { name
: 'filter11', label
: 'Hide filter 11', description
: '' },
491 { name
: 'filter12', label
: 'Hide filter 12', description
: '' }
506 // This is mocking the cases above, both
507 // - 'Two filters in one "send_unselected_if_any" group returns the other parameters truthy.'
508 // - 'Two filters selected in "string_option" group returns those filters in the value.'
510 group1__hidefilter1
: true,
511 group1__hidefilter2
: true,
512 group1__hidefilter3
: false,
513 group2__hidefilter4
: false,
514 group2__hidefilter5
: false,
515 group2__hidefilter6
: false,
516 group3__filter7
: true,
517 group3__filter8
: true,
518 group3__filter9
: false
520 expected
: $.extend( true, {}, baseResult
, {
521 // Group 1 (two selected, the others are true)
523 // Group 3 (two selected)
524 group3
: 'filter7,filter8'
526 msg
: 'Given an explicit (complete) filter state object, the result is the same as if the object given represented the model state.'
529 // This is mocking case above
530 // - 'One filter in one "send_unselected_if_any" group returns the other parameters truthy.'
532 group1__hidefilter1
: 1
534 expected
: $.extend( true, {}, baseResult
, {
535 // Group 1 (one selected, the others are true)
539 msg
: 'Given an explicit (incomplete) filter state object, the result is the same as if the object give represented the model state.'
543 group4__filter10
: true
545 expected
: $.extend( true, {}, baseResult
, {
548 msg
: 'Given a single value for "single_option" that option is represented in the result.'
552 group4__filter10
: true,
553 group4__filter11
: true
555 expected
: $.extend( true, {}, baseResult
, {
558 msg
: 'Given more than one true value for "single_option" (which should not happen!) only the first value counts, and the second is ignored.'
562 expected
: baseResult
,
563 msg
: 'Given an explicit empty object, the result is all filters set to their falsey unselected value.'
567 model
.initializeFilters( definition
);
568 // Store original state
569 originalState
= model
.getSelectedState();
572 cases
.forEach( function ( test
) {
574 model
.getParametersFromFilters( test
.input
),
580 // After doing the above tests, make sure the actual state
581 // of the filter stayed the same
583 model
.getSelectedState(),
585 'Running the method with external definition to parse does not actually change the state of the model'
589 QUnit
.test( 'getFiltersFromParameters', function ( assert
) {
590 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
592 model
.initializeFilters( filterDefinition
, viewsDefinition
);
594 // Empty query = only default values
596 model
.getFiltersFromParameters( {} ),
597 baseFilterRepresentation
,
598 'Empty parameter query results in an object representing all filters set to their base state'
602 model
.getFiltersFromParameters( {
605 $.extend( {}, baseFilterRepresentation
, {
606 group1__filter1
: true, // The text is "show filter 1"
607 group1__filter2
: false, // The text is "show filter 2"
608 group1__filter3
: true // The text is "show filter 3"
610 'One truthy parameter in a group whose other parameters are true by default makes the rest of the filters in the group false (unchecked)'
614 model
.getFiltersFromParameters( {
619 $.extend( {}, baseFilterRepresentation
, {
620 group1__filter1
: false, // The text is "show filter 1"
621 group1__filter2
: false, // The text is "show filter 2"
622 group1__filter3
: false // The text is "show filter 3"
624 'All paremeters in the same \'send_unselected_if_any\' group false is equivalent to none are truthy (checked) in the interface'
627 // The ones above don't update the model, so we have a clean state.
628 // getFiltersFromParameters is stateless; any change is unaffected by the current state
629 // This test is demonstrating wrong usage of the method;
630 // We should be aware that getFiltersFromParameters is stateless,
631 // so each call gives us a filter state that only reflects the query given.
632 // This means that the two calls to toggleFiltersSelected() below collide.
633 // The result of the first is overridden by the result of the second,
634 // since both get a full state object from getFiltersFromParameters that **only** relates
635 // to the input it receives.
636 model
.toggleFiltersSelected(
637 model
.getFiltersFromParameters( {
642 model
.toggleFiltersSelected(
643 model
.getFiltersFromParameters( {
648 // The result here is ignoring the first toggleFiltersSelected call
650 model
.getSelectedState(),
651 $.extend( {}, baseFilterRepresentation
, {
652 group2__filter4
: true,
653 group2__filter5
: true,
654 group2__filter6
: false
656 'getFiltersFromParameters does not care about previous or existing state.'
660 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
661 model
.initializeFilters( filterDefinition
, viewsDefinition
);
663 model
.toggleFiltersSelected(
664 model
.getFiltersFromParameters( {
669 model
.getSelectedState(),
670 $.extend( {}, baseFilterRepresentation
, {
671 group3__filter7
: true,
672 group3__filter8
: false,
673 group3__filter9
: false
675 'A \'string_options\' parameter containing 1 value, results in the corresponding filter as checked'
678 model
.toggleFiltersSelected(
679 model
.getFiltersFromParameters( {
680 group3
: 'filter7,filter8'
684 model
.getSelectedState(),
685 $.extend( {}, baseFilterRepresentation
, {
686 group3__filter7
: true,
687 group3__filter8
: true,
688 group3__filter9
: false
690 'A \'string_options\' parameter containing 2 values, results in both corresponding filters as checked'
693 model
.toggleFiltersSelected(
694 model
.getFiltersFromParameters( {
695 group3
: 'filter7,filter8,filter9'
699 model
.getSelectedState(),
700 $.extend( {}, baseFilterRepresentation
, {
701 group3__filter7
: true,
702 group3__filter8
: true,
703 group3__filter9
: true
705 'A \'string_options\' parameter containing all values, results in all filters of the group as checked.'
708 model
.toggleFiltersSelected(
709 model
.getFiltersFromParameters( {
710 group3
: 'filter7,all,filter9'
714 model
.getSelectedState(),
715 $.extend( {}, baseFilterRepresentation
, {
716 group3__filter7
: true,
717 group3__filter8
: true,
718 group3__filter9
: true
720 'A \'string_options\' parameter containing the value \'all\', results in all filters of the group as checked.'
723 model
.toggleFiltersSelected(
724 model
.getFiltersFromParameters( {
725 group3
: 'filter7,foo,filter9'
729 model
.getSelectedState(),
730 $.extend( {}, baseFilterRepresentation
, {
731 group3__filter7
: true,
732 group3__filter8
: false,
733 group3__filter9
: true
735 'A \'string_options\' parameter containing an invalid value, results in the invalid value ignored and the valid corresponding filters checked.'
738 model
.toggleFiltersSelected(
739 model
.getFiltersFromParameters( {
744 model
.getSelectedState(),
745 $.extend( {}, baseFilterRepresentation
, {
746 group4__option1
: true,
747 group4__option2
: false
749 'A \'single_option\' parameter reflects a single selected value.'
753 model
.getFiltersFromParameters( {
754 group4
: 'option1,option2'
756 baseFilterRepresentation
,
757 'An invalid \'single_option\' parameter is ignored.'
760 // Change to one value
761 model
.toggleFiltersSelected(
762 model
.getFiltersFromParameters( {
766 // Change again to another value
767 model
.toggleFiltersSelected(
768 model
.getFiltersFromParameters( {
773 model
.getSelectedState(),
774 $.extend( {}, baseFilterRepresentation
, {
775 group4__option2
: true
777 'A \'single_option\' parameter always reflects the latest selected value.'
781 QUnit
.test( 'sanitizeStringOptionGroup', function ( assert
) {
782 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
784 model
.initializeFilters( filterDefinition
, viewsDefinition
);
787 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'filter1', 'filter2' ] ),
788 [ 'filter1', 'filter2' ],
789 'Remove duplicate values'
793 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'foo', 'filter2' ] ),
794 [ 'filter1', 'filter2' ],
795 'Remove invalid values'
799 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'all', 'filter2' ] ),
801 'If any value is "all", the only value is "all".'
805 QUnit
.test( 'Filter interaction: subsets', function ( assert
) {
806 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
808 model
.initializeFilters( filterDefinition
, viewsDefinition
);
810 // Select a filter that has subset with another filter
811 model
.toggleFiltersSelected( {
812 group1__filter1
: true
815 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
817 model
.getFullState(),
818 $.extend( true, {}, baseFullFilterState
, {
819 group1__filter1
: { selected
: true },
820 group1__filter2
: { included
: true },
821 group1__filter3
: { included
: true },
822 // Conflicts are affected
823 group2__filter4
: { conflicted
: true },
824 group2__filter5
: { conflicted
: true },
825 group2__filter6
: { conflicted
: true }
827 'Filters with subsets are represented in the model.'
830 // Select another filter that has a subset with the same previous filter
831 model
.toggleFiltersSelected( {
832 group1__filter2
: true
834 model
.reassessFilterInteractions( model
.getItemByName( 'filter2' ) );
836 model
.getFullState(),
837 $.extend( true, {}, baseFullFilterState
, {
838 group1__filter1
: { selected
: true },
839 group1__filter2
: { selected
: true, included
: true },
840 group1__filter3
: { included
: true },
841 // Conflicts are affected
842 group2__filter6
: { conflicted
: true }
844 'Filters that have multiple subsets are represented.'
847 // Remove one filter (but leave the other) that affects filter3
848 model
.toggleFiltersSelected( {
849 group1__filter1
: false
851 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
853 model
.getFullState(),
854 $.extend( true, {}, baseFullFilterState
, {
855 group1__filter2
: { selected
: true, included
: false },
856 group1__filter3
: { included
: true },
857 // Conflicts are affected
858 group2__filter6
: { conflicted
: true }
860 'Removing a filter only un-includes its subset if there is no other filter affecting.'
863 model
.toggleFiltersSelected( {
864 group1__filter2
: false
866 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
868 model
.getFullState(),
870 'Removing all supersets also un-includes the subsets.'
874 QUnit
.test( 'Filter interaction: full coverage', function ( assert
) {
875 var model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
876 isCapsuleItemMuted = function ( filterName
) {
877 var itemModel
= model
.getItemByName( filterName
),
878 groupModel
= itemModel
.getGroupModel();
880 // This is the logic inside the capsule widget
882 // The capsule item widget only appears if the item is selected
883 itemModel
.isSelected() &&
884 // Muted state is only valid if group is full coverage and all items are selected
885 groupModel
.isFullCoverage() && groupModel
.areAllSelected()
888 getCurrentItemsMutedState = function () {
890 group1__filter1
: isCapsuleItemMuted( 'group1__filter1' ),
891 group1__filter2
: isCapsuleItemMuted( 'group1__filter2' ),
892 group1__filter3
: isCapsuleItemMuted( 'group1__filter3' ),
893 group2__filter4
: isCapsuleItemMuted( 'group2__filter4' ),
894 group2__filter5
: isCapsuleItemMuted( 'group2__filter5' ),
895 group2__filter6
: isCapsuleItemMuted( 'group2__filter6' )
899 group1__filter1
: false,
900 group1__filter2
: false,
901 group1__filter3
: false,
902 group2__filter4
: false,
903 group2__filter5
: false,
904 group2__filter6
: false
907 model
.initializeFilters( filterDefinition
, viewsDefinition
);
909 // Starting state, no selection, all items are non-muted
911 getCurrentItemsMutedState(),
913 'No selection - all items are non-muted'
916 // Select most (but not all) items in each group
917 model
.toggleFiltersSelected( {
918 group1__filter1
: true,
919 group1__filter2
: true,
920 group2__filter4
: true,
921 group2__filter5
: true
924 // Both groups have multiple (but not all) items selected, all items are non-muted
926 getCurrentItemsMutedState(),
928 'Not all items in the group selected - all items are non-muted'
931 // Select all items in 'fullCoverage' group (group2)
932 model
.toggleFiltersSelected( {
933 group2__filter6
: true
936 // Group2 (full coverage) has all items selected, all its items are muted
938 getCurrentItemsMutedState(),
939 $.extend( {}, baseMuteState
, {
940 group2__filter4
: true,
941 group2__filter5
: true,
942 group2__filter6
: true
944 'All items in \'full coverage\' group are selected - all items in the group are muted'
947 // Select all items in non 'fullCoverage' group (group1)
948 model
.toggleFiltersSelected( {
949 group1__filter3
: true
952 // Group1 (full coverage) has all items selected, no items in it are muted (non full coverage)
954 getCurrentItemsMutedState(),
955 $.extend( {}, baseMuteState
, {
956 group2__filter4
: true,
957 group2__filter5
: true,
958 group2__filter6
: true
960 'All items in a non \'full coverage\' group are selected - none of the items in the group are muted'
963 // Uncheck an item from each group
964 model
.toggleFiltersSelected( {
965 group1__filter3
: false,
966 group2__filter5
: false
969 getCurrentItemsMutedState(),
971 'Not all items in the group are checked - all items are non-muted regardless of group coverage'
975 QUnit
.test( 'Filter interaction: conflicts', function ( assert
) {
976 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
978 model
.initializeFilters( filterDefinition
, viewsDefinition
);
981 model
.getFullState(),
983 'Initial state: no conflicts because no selections.'
986 // Select a filter that has a conflict with an entire group
987 model
.toggleFiltersSelected( {
988 group1__filter1
: true // conflicts: entire of group 2 ( filter4, filter5, filter6)
991 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
994 model
.getFullState(),
995 $.extend( true, {}, baseFullFilterState
, {
996 group1__filter1
: { selected
: true },
997 group2__filter4
: { conflicted
: true },
998 group2__filter5
: { conflicted
: true },
999 group2__filter6
: { conflicted
: true },
1000 // Subsets are affected by the selection
1001 group1__filter2
: { included
: true },
1002 group1__filter3
: { included
: true }
1004 'Selecting a filter that conflicts with a group sets all the conflicted group items as "conflicted".'
1007 // Select one of the conflicts (both filters are now conflicted and selected)
1008 model
.toggleFiltersSelected( {
1009 group2__filter4
: true // conflicts: filter 1
1011 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter4' ) );
1014 model
.getFullState(),
1015 $.extend( true, {}, baseFullFilterState
, {
1016 group1__filter1
: { selected
: true, conflicted
: true },
1017 group2__filter4
: { selected
: true, conflicted
: true },
1018 group2__filter5
: { conflicted
: true },
1019 group2__filter6
: { conflicted
: true },
1020 // Subsets are affected by the selection
1021 group1__filter2
: { included
: true },
1022 group1__filter3
: { included
: true }
1024 'Selecting a conflicting filter inside a group, sets both sides to conflicted and selected.'
1028 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1029 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1031 // Select a filter that has a conflict with a specific filter
1032 model
.toggleFiltersSelected( {
1033 group1__filter2
: true // conflicts: filter6
1035 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1038 model
.getFullState(),
1039 $.extend( true, {}, baseFullFilterState
, {
1040 group1__filter2
: { selected
: true },
1041 group2__filter6
: { conflicted
: true },
1042 // Subsets are affected by the selection
1043 group1__filter3
: { included
: true }
1045 'Selecting a filter that conflicts with another filter sets the other as "conflicted".'
1048 // Select the conflicting filter
1049 model
.toggleFiltersSelected( {
1050 group2__filter6
: true // conflicts: filter2
1053 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter6' ) );
1056 model
.getFullState(),
1057 $.extend( true, {}, baseFullFilterState
, {
1058 group1__filter2
: { selected
: true, conflicted
: true },
1059 group2__filter6
: { selected
: true, conflicted
: true },
1060 // This is added to the conflicts because filter6 is part of group2,
1061 // who is in conflict with filter1; note that filter2 also conflicts
1062 // with filter6 which means that filter1 conflicts with filter6 (because it's in group2)
1063 // and also because its **own sibling** (filter2) is **also** in conflict with the
1064 // selected items in group2 (filter6)
1065 group1__filter1
: { conflicted
: true },
1067 // Subsets are affected by the selection
1068 group1__filter3
: { included
: true }
1070 'Selecting a conflicting filter with an individual filter, sets both sides to conflicted and selected.'
1073 // Now choose a non-conflicting filter from the group
1074 model
.toggleFiltersSelected( {
1075 group2__filter5
: true
1078 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter5' ) );
1081 model
.getFullState(),
1082 $.extend( true, {}, baseFullFilterState
, {
1083 group1__filter2
: { selected
: true },
1084 group2__filter6
: { selected
: true },
1085 group2__filter5
: { selected
: true },
1086 // Filter6 and filter1 are no longer in conflict because
1087 // filter5, while it is in conflict with filter1, it is
1088 // not in conflict with filter2 - and since filter2 is
1089 // selected, it removes the conflict bidirectionally
1091 // Subsets are affected by the selection
1092 group1__filter3
: { included
: true }
1094 'Selecting a non-conflicting filter within the group of a conflicting filter removes the conflicts.'
1097 // Followup on the previous test, unselect filter2 so filter1
1098 // is now the only one selected in its own group, and since
1099 // it is in conflict with the entire of group2, it means
1100 // filter1 is once again conflicted
1101 model
.toggleFiltersSelected( {
1102 group1__filter2
: false
1105 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1108 model
.getFullState(),
1109 $.extend( true, {}, baseFullFilterState
, {
1110 group1__filter1
: { conflicted
: true },
1111 group2__filter6
: { selected
: true },
1112 group2__filter5
: { selected
: true }
1114 'Unselecting an item that did not conflict returns the conflict state.'
1117 // Followup #2: Now actually select filter1, and make everything conflicted
1118 model
.toggleFiltersSelected( {
1119 group1__filter1
: true
1122 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
1125 model
.getFullState(),
1126 $.extend( true, {}, baseFullFilterState
, {
1127 group1__filter1
: { selected
: true, conflicted
: true },
1128 group2__filter6
: { selected
: true, conflicted
: true },
1129 group2__filter5
: { selected
: true, conflicted
: true },
1130 group2__filter4
: { conflicted
: true }, // Not selected but conflicted because it's in group2
1131 // Subsets are affected by the selection
1132 group1__filter2
: { included
: true },
1133 group1__filter3
: { included
: true }
1135 'Selecting an item that conflicts with a whole group makes all selections in that group conflicted.'
1140 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1141 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1143 // Select a filter that has a conflict with a specific filter
1144 model
.toggleFiltersSelected( {
1145 group1__filter2
: true // conflicts: filter6
1148 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1151 model
.getFullState(),
1152 $.extend( true, {}, baseFullFilterState
, {
1153 group1__filter2
: { selected
: true },
1154 group2__filter6
: { conflicted
: true },
1155 // Subsets are affected by the selection
1156 group1__filter3
: { included
: true }
1158 'Simple case: Selecting a filter that conflicts with another filter sets the other as "conflicted".'
1161 model
.toggleFiltersSelected( {
1162 group1__filter3
: true // conflicts: filter6
1165 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter3' ) );
1168 model
.getFullState(),
1169 $.extend( true, {}, baseFullFilterState
, {
1170 group1__filter2
: { selected
: true },
1171 // Subsets are affected by the selection
1172 group1__filter3
: { selected
: true, included
: true }
1174 'Simple case: Selecting a filter that is not in conflict removes the conflict.'
1178 QUnit
.test( 'Filter highlights', function ( assert
) {
1179 // We are using a different (smaller) definition here than the global one
1180 var definition
= [ {
1183 type
: 'string_options',
1185 { name
: 'filter1', cssClass
: 'class1', label
: '1', description
: '1' },
1186 { name
: 'filter2', cssClass
: 'class2', label
: '2', description
: '2' },
1187 { name
: 'filter3', cssClass
: 'class3', label
: '3', description
: '3' },
1188 { name
: 'filter4', cssClass
: 'class4', label
: '4', description
: '4' },
1189 { name
: 'filter5', cssClass
: 'class5', label
: '5', description
: '5' },
1190 { name
: 'filter6', label
: '6', description
: '6' }
1193 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1195 model
.initializeFilters( definition
);
1198 !model
.isHighlightEnabled(),
1199 'Initially, highlight is disabled.'
1202 model
.toggleHighlight( true );
1204 model
.isHighlightEnabled(),
1205 'Highlight is enabled on toggle.'
1208 model
.setHighlightColor( 'group1__filter1', 'color1' );
1209 model
.setHighlightColor( 'group1__filter2', 'color2' );
1212 model
.getHighlightedItems().map( function ( item
) {
1213 return item
.getName();
1219 'Highlighted items are highlighted.'
1223 model
.getItemByName( 'group1__filter1' ).getHighlightColor(),
1225 'Item highlight color is set.'
1228 model
.setHighlightColor( 'group1__filter1', 'color1changed' );
1230 model
.getItemByName( 'group1__filter1' ).getHighlightColor(),
1232 'Item highlight color is changed on setHighlightColor.'
1235 model
.clearHighlightColor( 'group1__filter1' );
1237 model
.getHighlightedItems().map( function ( item
) {
1238 return item
.getName();
1243 'Clear highlight from an item results in the item no longer being highlighted.'
1247 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1248 model
.initializeFilters( definition
);
1250 model
.setHighlightColor( 'group1__filter1', 'color1' );
1251 model
.setHighlightColor( 'group1__filter2', 'color2' );
1252 model
.setHighlightColor( 'group1__filter3', 'color3' );
1255 model
.getHighlightedItems().map( function ( item
) {
1256 return item
.getName();
1263 'Even if highlights are not enabled, the items remember their highlight state'
1264 // NOTE: When actually displaying the highlights, the UI checks whether
1265 // highlighting is generally active and then goes over the highlighted
1266 // items. The item models, however, and the view model in general, still
1267 // retains the knowledge about which filters have different colors, so we
1268 // can seamlessly return to the colors the user previously chose if they
1269 // reapply highlights.
1273 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1274 model
.initializeFilters( definition
);
1276 model
.setHighlightColor( 'group1__filter1', 'color1' );
1277 model
.setHighlightColor( 'group1__filter6', 'color6' );
1280 model
.getHighlightedItems().map( function ( item
) {
1281 return item
.getName();
1286 'Items without a specified class identifier are not highlighted.'
1289 }( mediaWiki
, jQuery
) );