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' }
82 type
: 'string_options',
85 { name
: 0, label
: 'Main' },
86 { name
: 1, label
: 'Talk' },
87 { name
: 2, label
: 'User' },
88 { name
: 3, label
: 'User talk' }
105 baseParamRepresentation
= {
117 baseFilterRepresentation
= {
118 group1__filter1
: false,
119 group1__filter2
: false,
120 group1__filter3
: false,
121 group2__filter4
: false,
122 group2__filter5
: false,
123 group2__filter6
: false,
124 group3__filter7
: false,
125 group3__filter8
: false,
126 group3__filter9
: false,
127 // The 'single_value' type of group can't have empty value; it's either
128 // the default given or the first item that will get the truthy value
129 group4__option1
: false,
130 group4__option2
: true, // Default
131 group4__option3
: false,
132 group5__option1
: true, // No default set, first item is default value
133 group5__option2
: false,
134 group5__option3
: false,
140 baseFullFilterState
= {
141 group1__filter1
: { selected
: false, conflicted
: false, included
: false },
142 group1__filter2
: { selected
: false, conflicted
: false, included
: false },
143 group1__filter3
: { selected
: false, conflicted
: false, included
: false },
144 group2__filter4
: { selected
: false, conflicted
: false, included
: false },
145 group2__filter5
: { selected
: false, conflicted
: false, included
: false },
146 group2__filter6
: { selected
: false, conflicted
: false, included
: false },
147 group3__filter7
: { selected
: false, conflicted
: false, included
: false },
148 group3__filter8
: { selected
: false, conflicted
: false, included
: false },
149 group3__filter9
: { selected
: false, conflicted
: false, included
: false },
150 group4__option1
: { selected
: false, conflicted
: false, included
: false },
151 group4__option2
: { selected
: true, conflicted
: false, included
: false },
152 group4__option3
: { selected
: false, conflicted
: false, included
: false },
153 group5__option1
: { selected
: true, conflicted
: false, included
: false },
154 group5__option2
: { selected
: false, conflicted
: false, included
: false },
155 group5__option3
: { selected
: false, conflicted
: false, included
: false },
156 namespace__0
: { selected
: false, conflicted
: false, included
: false },
157 namespace__1
: { selected
: false, conflicted
: false, included
: false },
158 namespace__2
: { selected
: false, conflicted
: false, included
: false },
159 namespace__3
: { selected
: false, conflicted
: false, included
: false }
162 QUnit
.module( 'mediawiki.rcfilters - FiltersViewModel', QUnit
.newMwEnvironment( {
164 'group1filter1-label': 'Group 1: Filter 1 title',
165 'group1filter1-desc': 'Description of Filter 1 in Group 1',
166 'group1filter2-label': 'Group 1: Filter 2 title',
167 'group1filter2-desc': 'Description of Filter 2 in Group 1',
168 'group1filter3-label': 'Group 1: Filter 3',
169 'group1filter3-desc': 'Description of Filter 3 in Group 1',
171 'group2filter4-label': 'Group 2: Filter 4 title',
172 'group2filter4-desc': 'Description of Filter 4 in Group 2',
173 'group2filter5-label': 'Group 2: Filter 5',
174 'group2filter5-desc': 'Description of Filter 5 in Group 2',
175 'group2filter6-label': 'xGroup 2: Filter 6',
176 'group2filter6-desc': 'Description of Filter 6 in Group 2'
179 wgStructuredChangeFiltersEnableExperimentalViews
: true
183 QUnit
.test( 'Setting up filters', function ( assert
) {
184 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
186 model
.initializeFilters( filterDefinition
, viewsDefinition
);
188 // Test that all items were created
190 Object
.keys( baseFilterRepresentation
).every( function ( filterName
) {
191 return model
.getItemByName( filterName
) instanceof mw
.rcfilters
.dm
.FilterItem
;
193 'Filters instantiated and stored correctly'
197 model
.getSelectedState(),
198 baseFilterRepresentation
,
199 'Initial state of filters'
202 model
.toggleFiltersSelected( {
203 group1__filter1
: true,
204 group2__filter5
: true,
205 group3__filter7
: true
208 model
.getSelectedState(),
209 $.extend( true, {}, baseFilterRepresentation
, {
210 group1__filter1
: true,
211 group2__filter5
: true,
212 group3__filter7
: true
214 'Updating filter states correctly'
218 QUnit
.test( 'Default filters', function ( assert
) {
219 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
221 model
.initializeFilters( filterDefinition
, viewsDefinition
);
223 // Empty query = only default values
225 model
.getDefaultParams(),
227 'Default parameters are stored properly per filter and group'
231 QUnit
.test( 'Finding matching filters', function ( assert
) {
237 group1
: [ 'group1__filter1', 'group1__filter2', 'group1__filter3' ],
238 group2
: [ 'group2__filter4', 'group2__filter5' ]
240 reason
: 'Finds filters starting with the query string'
245 group2
: [ 'group2__filter4', 'group2__filter5', 'group2__filter6' ]
247 reason
: 'Finds filters containing the query string in their description'
252 group1
: [ 'group1__filter1', 'group1__filter2' ],
253 group2
: [ 'group2__filter4' ]
255 reason
: 'Finds filters containing the query string in their group title'
260 namespace: [ 'namespace__0' ]
262 reason
: 'Finds item in view when a prefix is used'
267 reason
: 'Finds no results if using namespaces prefix (:) to search for filter title'
270 model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
271 extractNames = function ( matches
) {
273 Object
.keys( matches
).forEach( function ( groupName
) {
274 result
[ groupName
] = matches
[ groupName
].map( function ( item
) {
275 return item
.getName();
281 model
.initializeFilters( filterDefinition
, viewsDefinition
);
283 testCases
.forEach( function ( testCase
) {
284 matches
= model
.findMatches( testCase
.query
);
286 extractNames( matches
),
287 testCase
.expectedMatches
,
292 matches
= model
.findMatches( 'foo' );
294 $.isEmptyObject( matches
),
295 'findMatches returns an empty object when no results found'
299 QUnit
.test( 'getParametersFromFilters', function ( assert
) {
300 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
302 model
.initializeFilters( filterDefinition
, viewsDefinition
);
304 // Starting with all filters unselected
306 model
.getParametersFromFilters(),
307 baseParamRepresentation
,
308 'Unselected filters return all parameters falsey or \'\'.'
312 model
.toggleFiltersSelected( {
313 group1__filter1
: true
315 // Only one filter in one group
317 model
.getParametersFromFilters(),
318 $.extend( true, {}, baseParamRepresentation
, {
319 // Group 1 (one selected, the others are true)
323 'One filter in one "send_unselected_if_any" group returns the other parameters truthy.'
327 model
.toggleFiltersSelected( {
328 group1__filter1
: true,
329 group1__filter2
: true
331 // Two selected filters in one group
333 model
.getParametersFromFilters(),
334 $.extend( true, {}, baseParamRepresentation
, {
335 // Group 1 (two selected, the other is true)
338 'Two filters in one "send_unselected_if_any" group returns the other parameters truthy.'
342 model
.toggleFiltersSelected( {
343 group1__filter1
: true,
344 group1__filter2
: true,
345 group1__filter3
: true
347 // All filters of the group are selected == this is the same as not selecting any
349 model
.getParametersFromFilters(),
350 baseParamRepresentation
,
351 'All filters selected in one "send_unselected_if_any" group returns all parameters falsy.'
354 // Select 1 filter from string_options
355 model
.toggleFiltersSelected( {
356 group3__filter7
: true,
357 group3__filter8
: false,
358 group3__filter9
: false
360 // All filters of the group are selected == this is the same as not selecting any
362 model
.getParametersFromFilters(),
363 $.extend( true, {}, baseParamRepresentation
, {
366 'One filter selected in "string_option" group returns that filter in the value.'
369 // Select 2 filters from string_options
370 model
.toggleFiltersSelected( {
371 group3__filter7
: true,
372 group3__filter8
: true,
373 group3__filter9
: false
375 // All filters of the group are selected == this is the same as not selecting any
377 model
.getParametersFromFilters(),
378 $.extend( true, {}, baseParamRepresentation
, {
379 group3
: 'filter7,filter8'
381 'Two filters selected in "string_option" group returns those filters in the value.'
384 // Select 3 filters from string_options
385 model
.toggleFiltersSelected( {
386 group3__filter7
: true,
387 group3__filter8
: true,
388 group3__filter9
: true
390 // All filters of the group are selected == this is the same as not selecting any
392 model
.getParametersFromFilters(),
393 $.extend( true, {}, baseParamRepresentation
, {
396 'All filters selected in "string_option" group returns \'all\'.'
400 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
401 model
.initializeFilters( filterDefinition
, viewsDefinition
);
403 // Select an option from single_option group
404 model
.toggleFiltersSelected( {
405 group4__option2
: true
407 // All filters of the group are selected == this is the same as not selecting any
409 model
.getParametersFromFilters(),
410 $.extend( true, {}, baseParamRepresentation
, {
413 'Selecting an option from "single_option" group returns that option as a value.'
416 // Select a different option from single_option group
417 model
.toggleFiltersSelected( {
418 group4__option3
: true
420 // All filters of the group are selected == this is the same as not selecting any
422 model
.getParametersFromFilters(),
423 $.extend( true, {}, baseParamRepresentation
, {
426 'Selecting a different option from "single_option" group changes the selection.'
430 QUnit
.test( 'getParametersFromFilters (custom object)', function ( assert
) {
431 // This entire test uses different base definition than the global one
432 // on purpose, to verify that the values inserted as a custom object
433 // are the ones we expect in return
435 model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
439 type
: 'send_unselected_if_any',
441 { name
: 'hidefilter1', label
: 'Hide filter 1', description
: '' },
442 { name
: 'hidefilter2', label
: 'Hide filter 2', description
: '' },
443 { name
: 'hidefilter3', label
: 'Hide filter 3', description
: '' }
448 type
: 'send_unselected_if_any',
450 { name
: 'hidefilter4', label
: 'Hide filter 4', description
: '' },
451 { name
: 'hidefilter5', label
: 'Hide filter 5', description
: '' },
452 { name
: 'hidefilter6', label
: 'Hide filter 6', description
: '' }
457 type
: 'string_options',
460 { name
: 'filter7', label
: 'Hide filter 7', description
: '' },
461 { name
: 'filter8', label
: 'Hide filter 8', description
: '' },
462 { name
: 'filter9', label
: 'Hide filter 9', description
: '' }
467 type
: 'single_option',
469 { name
: 'filter10', label
: 'Hide filter 10', description
: '' },
470 { name
: 'filter11', label
: 'Hide filter 11', description
: '' },
471 { name
: 'filter12', label
: 'Hide filter 12', description
: '' }
486 // This is mocking the cases above, both
487 // - 'Two filters in one "send_unselected_if_any" group returns the other parameters truthy.'
488 // - 'Two filters selected in "string_option" group returns those filters in the value.'
490 group1__hidefilter1
: true,
491 group1__hidefilter2
: true,
492 group1__hidefilter3
: false,
493 group2__hidefilter4
: false,
494 group2__hidefilter5
: false,
495 group2__hidefilter6
: false,
496 group3__filter7
: true,
497 group3__filter8
: true,
498 group3__filter9
: false
500 expected
: $.extend( true, {}, baseResult
, {
501 // Group 1 (two selected, the others are true)
503 // Group 3 (two selected)
504 group3
: 'filter7,filter8'
506 msg
: 'Given an explicit (complete) filter state object, the result is the same as if the object given represented the model state.'
509 // This is mocking case above
510 // - 'One filter in one "send_unselected_if_any" group returns the other parameters truthy.'
512 group1__hidefilter1
: 1
514 expected
: $.extend( true, {}, baseResult
, {
515 // Group 1 (one selected, the others are true)
519 msg
: 'Given an explicit (incomplete) filter state object, the result is the same as if the object give represented the model state.'
523 group4__filter10
: true
525 expected
: $.extend( true, {}, baseResult
, {
528 msg
: 'Given a single value for "single_option" that option is represented in the result.'
532 group4__filter10
: true,
533 group4__filter11
: true
535 expected
: $.extend( true, {}, baseResult
, {
538 msg
: 'Given more than one true value for "single_option" (which should not happen!) only the first value counts, and the second is ignored.'
542 expected
: baseResult
,
543 msg
: 'Given an explicit empty object, the result is all filters set to their falsey unselected value.'
547 model
.initializeFilters( definition
);
548 // Store original state
549 originalState
= model
.getSelectedState();
552 cases
.forEach( function ( test
) {
554 model
.getParametersFromFilters( test
.input
),
560 // After doing the above tests, make sure the actual state
561 // of the filter stayed the same
563 model
.getSelectedState(),
565 'Running the method with external definition to parse does not actually change the state of the model'
569 QUnit
.test( 'getFiltersFromParameters', function ( assert
) {
570 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
572 model
.initializeFilters( filterDefinition
, viewsDefinition
);
574 // Empty query = only default values
576 model
.getFiltersFromParameters( {} ),
577 baseFilterRepresentation
,
578 'Empty parameter query results in an object representing all filters set to their base state'
582 model
.getFiltersFromParameters( {
585 $.extend( {}, baseFilterRepresentation
, {
586 group1__filter1
: true, // The text is "show filter 1"
587 group1__filter2
: false, // The text is "show filter 2"
588 group1__filter3
: true // The text is "show filter 3"
590 'One truthy parameter in a group whose other parameters are true by default makes the rest of the filters in the group false (unchecked)'
594 model
.getFiltersFromParameters( {
599 $.extend( {}, baseFilterRepresentation
, {
600 group1__filter1
: false, // The text is "show filter 1"
601 group1__filter2
: false, // The text is "show filter 2"
602 group1__filter3
: false // The text is "show filter 3"
604 'All paremeters in the same \'send_unselected_if_any\' group false is equivalent to none are truthy (checked) in the interface'
607 // The ones above don't update the model, so we have a clean state.
608 // getFiltersFromParameters is stateless; any change is unaffected by the current state
609 // This test is demonstrating wrong usage of the method;
610 // We should be aware that getFiltersFromParameters is stateless,
611 // so each call gives us a filter state that only reflects the query given.
612 // This means that the two calls to toggleFiltersSelected() below collide.
613 // The result of the first is overridden by the result of the second,
614 // since both get a full state object from getFiltersFromParameters that **only** relates
615 // to the input it receives.
616 model
.toggleFiltersSelected(
617 model
.getFiltersFromParameters( {
622 model
.toggleFiltersSelected(
623 model
.getFiltersFromParameters( {
628 // The result here is ignoring the first toggleFiltersSelected call
630 model
.getSelectedState(),
631 $.extend( {}, baseFilterRepresentation
, {
632 group2__filter4
: true,
633 group2__filter5
: true,
634 group2__filter6
: false
636 'getFiltersFromParameters does not care about previous or existing state.'
640 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
641 model
.initializeFilters( filterDefinition
, viewsDefinition
);
643 model
.toggleFiltersSelected(
644 model
.getFiltersFromParameters( {
649 model
.getSelectedState(),
650 $.extend( {}, baseFilterRepresentation
, {
651 group3__filter7
: true,
652 group3__filter8
: false,
653 group3__filter9
: false
655 'A \'string_options\' parameter containing 1 value, results in the corresponding filter as checked'
658 model
.toggleFiltersSelected(
659 model
.getFiltersFromParameters( {
660 group3
: 'filter7,filter8'
664 model
.getSelectedState(),
665 $.extend( {}, baseFilterRepresentation
, {
666 group3__filter7
: true,
667 group3__filter8
: true,
668 group3__filter9
: false
670 'A \'string_options\' parameter containing 2 values, results in both corresponding filters as checked'
673 model
.toggleFiltersSelected(
674 model
.getFiltersFromParameters( {
675 group3
: 'filter7,filter8,filter9'
679 model
.getSelectedState(),
680 $.extend( {}, baseFilterRepresentation
, {
681 group3__filter7
: true,
682 group3__filter8
: true,
683 group3__filter9
: true
685 'A \'string_options\' parameter containing all values, results in all filters of the group as checked.'
688 model
.toggleFiltersSelected(
689 model
.getFiltersFromParameters( {
690 group3
: 'filter7,all,filter9'
694 model
.getSelectedState(),
695 $.extend( {}, baseFilterRepresentation
, {
696 group3__filter7
: true,
697 group3__filter8
: true,
698 group3__filter9
: true
700 'A \'string_options\' parameter containing the value \'all\', results in all filters of the group as checked.'
703 model
.toggleFiltersSelected(
704 model
.getFiltersFromParameters( {
705 group3
: 'filter7,foo,filter9'
709 model
.getSelectedState(),
710 $.extend( {}, baseFilterRepresentation
, {
711 group3__filter7
: true,
712 group3__filter8
: false,
713 group3__filter9
: true
715 'A \'string_options\' parameter containing an invalid value, results in the invalid value ignored and the valid corresponding filters checked.'
718 model
.toggleFiltersSelected(
719 model
.getFiltersFromParameters( {
724 model
.getSelectedState(),
725 $.extend( {}, baseFilterRepresentation
, {
726 group4__option1
: true,
727 group4__option2
: false
729 'A \'single_option\' parameter reflects a single selected value.'
733 model
.getFiltersFromParameters( {
734 group4
: 'option1,option2'
736 baseFilterRepresentation
,
737 'An invalid \'single_option\' parameter is ignored.'
740 // Change to one value
741 model
.toggleFiltersSelected(
742 model
.getFiltersFromParameters( {
746 // Change again to another value
747 model
.toggleFiltersSelected(
748 model
.getFiltersFromParameters( {
753 model
.getSelectedState(),
754 $.extend( {}, baseFilterRepresentation
, {
755 group4__option2
: true
757 'A \'single_option\' parameter always reflects the latest selected value.'
761 QUnit
.test( 'sanitizeStringOptionGroup', function ( assert
) {
762 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
764 model
.initializeFilters( filterDefinition
, viewsDefinition
);
767 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'filter1', 'filter2' ] ),
768 [ 'filter1', 'filter2' ],
769 'Remove duplicate values'
773 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'foo', 'filter2' ] ),
774 [ 'filter1', 'filter2' ],
775 'Remove invalid values'
779 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'all', 'filter2' ] ),
781 'If any value is "all", the only value is "all".'
785 QUnit
.test( 'Filter interaction: subsets', function ( assert
) {
786 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
788 model
.initializeFilters( filterDefinition
, viewsDefinition
);
790 // Select a filter that has subset with another filter
791 model
.toggleFiltersSelected( {
792 group1__filter1
: true
795 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
797 model
.getFullState(),
798 $.extend( true, {}, baseFullFilterState
, {
799 group1__filter1
: { selected
: true },
800 group1__filter2
: { included
: true },
801 group1__filter3
: { included
: true },
802 // Conflicts are affected
803 group2__filter4
: { conflicted
: true },
804 group2__filter5
: { conflicted
: true },
805 group2__filter6
: { conflicted
: true }
807 'Filters with subsets are represented in the model.'
810 // Select another filter that has a subset with the same previous filter
811 model
.toggleFiltersSelected( {
812 group1__filter2
: true
814 model
.reassessFilterInteractions( model
.getItemByName( 'filter2' ) );
816 model
.getFullState(),
817 $.extend( true, {}, baseFullFilterState
, {
818 group1__filter1
: { selected
: true },
819 group1__filter2
: { selected
: true, included
: true },
820 group1__filter3
: { included
: true },
821 // Conflicts are affected
822 group2__filter6
: { conflicted
: true }
824 'Filters that have multiple subsets are represented.'
827 // Remove one filter (but leave the other) that affects filter3
828 model
.toggleFiltersSelected( {
829 group1__filter1
: false
831 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
833 model
.getFullState(),
834 $.extend( true, {}, baseFullFilterState
, {
835 group1__filter2
: { selected
: true, included
: false },
836 group1__filter3
: { included
: true },
837 // Conflicts are affected
838 group2__filter6
: { conflicted
: true }
840 'Removing a filter only un-includes its subset if there is no other filter affecting.'
843 model
.toggleFiltersSelected( {
844 group1__filter2
: false
846 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
848 model
.getFullState(),
850 'Removing all supersets also un-includes the subsets.'
854 QUnit
.test( 'Filter interaction: full coverage', function ( assert
) {
855 var model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
856 isCapsuleItemMuted = function ( filterName
) {
857 var itemModel
= model
.getItemByName( filterName
),
858 groupModel
= itemModel
.getGroupModel();
860 // This is the logic inside the capsule widget
862 // The capsule item widget only appears if the item is selected
863 itemModel
.isSelected() &&
864 // Muted state is only valid if group is full coverage and all items are selected
865 groupModel
.isFullCoverage() && groupModel
.areAllSelected()
868 getCurrentItemsMutedState = function () {
870 group1__filter1
: isCapsuleItemMuted( 'group1__filter1' ),
871 group1__filter2
: isCapsuleItemMuted( 'group1__filter2' ),
872 group1__filter3
: isCapsuleItemMuted( 'group1__filter3' ),
873 group2__filter4
: isCapsuleItemMuted( 'group2__filter4' ),
874 group2__filter5
: isCapsuleItemMuted( 'group2__filter5' ),
875 group2__filter6
: isCapsuleItemMuted( 'group2__filter6' )
879 group1__filter1
: false,
880 group1__filter2
: false,
881 group1__filter3
: false,
882 group2__filter4
: false,
883 group2__filter5
: false,
884 group2__filter6
: false
887 model
.initializeFilters( filterDefinition
, viewsDefinition
);
889 // Starting state, no selection, all items are non-muted
891 getCurrentItemsMutedState(),
893 'No selection - all items are non-muted'
896 // Select most (but not all) items in each group
897 model
.toggleFiltersSelected( {
898 group1__filter1
: true,
899 group1__filter2
: true,
900 group2__filter4
: true,
901 group2__filter5
: true
904 // Both groups have multiple (but not all) items selected, all items are non-muted
906 getCurrentItemsMutedState(),
908 'Not all items in the group selected - all items are non-muted'
911 // Select all items in 'fullCoverage' group (group2)
912 model
.toggleFiltersSelected( {
913 group2__filter6
: true
916 // Group2 (full coverage) has all items selected, all its items are muted
918 getCurrentItemsMutedState(),
919 $.extend( {}, baseMuteState
, {
920 group2__filter4
: true,
921 group2__filter5
: true,
922 group2__filter6
: true
924 'All items in \'full coverage\' group are selected - all items in the group are muted'
927 // Select all items in non 'fullCoverage' group (group1)
928 model
.toggleFiltersSelected( {
929 group1__filter3
: true
932 // Group1 (full coverage) has all items selected, no items in it are muted (non full coverage)
934 getCurrentItemsMutedState(),
935 $.extend( {}, baseMuteState
, {
936 group2__filter4
: true,
937 group2__filter5
: true,
938 group2__filter6
: true
940 'All items in a non \'full coverage\' group are selected - none of the items in the group are muted'
943 // Uncheck an item from each group
944 model
.toggleFiltersSelected( {
945 group1__filter3
: false,
946 group2__filter5
: false
949 getCurrentItemsMutedState(),
951 'Not all items in the group are checked - all items are non-muted regardless of group coverage'
955 QUnit
.test( 'Filter interaction: conflicts', function ( assert
) {
956 var model
= new mw
.rcfilters
.dm
.FiltersViewModel();
958 model
.initializeFilters( filterDefinition
, viewsDefinition
);
961 model
.getFullState(),
963 'Initial state: no conflicts because no selections.'
966 // Select a filter that has a conflict with an entire group
967 model
.toggleFiltersSelected( {
968 group1__filter1
: true // conflicts: entire of group 2 ( filter4, filter5, filter6)
971 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
974 model
.getFullState(),
975 $.extend( true, {}, baseFullFilterState
, {
976 group1__filter1
: { selected
: true },
977 group2__filter4
: { conflicted
: true },
978 group2__filter5
: { conflicted
: true },
979 group2__filter6
: { conflicted
: true },
980 // Subsets are affected by the selection
981 group1__filter2
: { included
: true },
982 group1__filter3
: { included
: true }
984 'Selecting a filter that conflicts with a group sets all the conflicted group items as "conflicted".'
987 // Select one of the conflicts (both filters are now conflicted and selected)
988 model
.toggleFiltersSelected( {
989 group2__filter4
: true // conflicts: filter 1
991 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter4' ) );
994 model
.getFullState(),
995 $.extend( true, {}, baseFullFilterState
, {
996 group1__filter1
: { selected
: true, conflicted
: true },
997 group2__filter4
: { selected
: true, 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 conflicting filter inside a group, sets both sides to conflicted and selected.'
1008 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1009 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1011 // Select a filter that has a conflict with a specific filter
1012 model
.toggleFiltersSelected( {
1013 group1__filter2
: true // conflicts: filter6
1015 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1018 model
.getFullState(),
1019 $.extend( true, {}, baseFullFilterState
, {
1020 group1__filter2
: { selected
: true },
1021 group2__filter6
: { conflicted
: true },
1022 // Subsets are affected by the selection
1023 group1__filter3
: { included
: true }
1025 'Selecting a filter that conflicts with another filter sets the other as "conflicted".'
1028 // Select the conflicting filter
1029 model
.toggleFiltersSelected( {
1030 group2__filter6
: true // conflicts: filter2
1033 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter6' ) );
1036 model
.getFullState(),
1037 $.extend( true, {}, baseFullFilterState
, {
1038 group1__filter2
: { selected
: true, conflicted
: true },
1039 group2__filter6
: { selected
: true, conflicted
: true },
1040 // This is added to the conflicts because filter6 is part of group2,
1041 // who is in conflict with filter1; note that filter2 also conflicts
1042 // with filter6 which means that filter1 conflicts with filter6 (because it's in group2)
1043 // and also because its **own sibling** (filter2) is **also** in conflict with the
1044 // selected items in group2 (filter6)
1045 group1__filter1
: { conflicted
: true },
1047 // Subsets are affected by the selection
1048 group1__filter3
: { included
: true }
1050 'Selecting a conflicting filter with an individual filter, sets both sides to conflicted and selected.'
1053 // Now choose a non-conflicting filter from the group
1054 model
.toggleFiltersSelected( {
1055 group2__filter5
: true
1058 model
.reassessFilterInteractions( model
.getItemByName( 'group2__filter5' ) );
1061 model
.getFullState(),
1062 $.extend( true, {}, baseFullFilterState
, {
1063 group1__filter2
: { selected
: true },
1064 group2__filter6
: { selected
: true },
1065 group2__filter5
: { selected
: true },
1066 // Filter6 and filter1 are no longer in conflict because
1067 // filter5, while it is in conflict with filter1, it is
1068 // not in conflict with filter2 - and since filter2 is
1069 // selected, it removes the conflict bidirectionally
1071 // Subsets are affected by the selection
1072 group1__filter3
: { included
: true }
1074 'Selecting a non-conflicting filter within the group of a conflicting filter removes the conflicts.'
1077 // Followup on the previous test, unselect filter2 so filter1
1078 // is now the only one selected in its own group, and since
1079 // it is in conflict with the entire of group2, it means
1080 // filter1 is once again conflicted
1081 model
.toggleFiltersSelected( {
1082 group1__filter2
: false
1085 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1088 model
.getFullState(),
1089 $.extend( true, {}, baseFullFilterState
, {
1090 group1__filter1
: { conflicted
: true },
1091 group2__filter6
: { selected
: true },
1092 group2__filter5
: { selected
: true }
1094 'Unselecting an item that did not conflict returns the conflict state.'
1097 // Followup #2: Now actually select filter1, and make everything conflicted
1098 model
.toggleFiltersSelected( {
1099 group1__filter1
: true
1102 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter1' ) );
1105 model
.getFullState(),
1106 $.extend( true, {}, baseFullFilterState
, {
1107 group1__filter1
: { selected
: true, conflicted
: true },
1108 group2__filter6
: { selected
: true, conflicted
: true },
1109 group2__filter5
: { selected
: true, conflicted
: true },
1110 group2__filter4
: { conflicted
: true }, // Not selected but conflicted because it's in group2
1111 // Subsets are affected by the selection
1112 group1__filter2
: { included
: true },
1113 group1__filter3
: { included
: true }
1115 'Selecting an item that conflicts with a whole group makes all selections in that group conflicted.'
1120 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1121 model
.initializeFilters( filterDefinition
, viewsDefinition
);
1123 // Select a filter that has a conflict with a specific filter
1124 model
.toggleFiltersSelected( {
1125 group1__filter2
: true // conflicts: filter6
1128 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter2' ) );
1131 model
.getFullState(),
1132 $.extend( true, {}, baseFullFilterState
, {
1133 group1__filter2
: { selected
: true },
1134 group2__filter6
: { conflicted
: true },
1135 // Subsets are affected by the selection
1136 group1__filter3
: { included
: true }
1138 'Simple case: Selecting a filter that conflicts with another filter sets the other as "conflicted".'
1141 model
.toggleFiltersSelected( {
1142 group1__filter3
: true // conflicts: filter6
1145 model
.reassessFilterInteractions( model
.getItemByName( 'group1__filter3' ) );
1148 model
.getFullState(),
1149 $.extend( true, {}, baseFullFilterState
, {
1150 group1__filter2
: { selected
: true },
1151 // Subsets are affected by the selection
1152 group1__filter3
: { selected
: true, included
: true }
1154 'Simple case: Selecting a filter that is not in conflict removes the conflict.'
1158 QUnit
.test( 'Filter highlights', function ( assert
) {
1159 // We are using a different (smaller) definition here than the global one
1160 var definition
= [ {
1163 type
: 'string_options',
1165 { name
: 'filter1', cssClass
: 'class1', label
: '1', description
: '1' },
1166 { name
: 'filter2', cssClass
: 'class2', label
: '2', description
: '2' },
1167 { name
: 'filter3', cssClass
: 'class3', label
: '3', description
: '3' },
1168 { name
: 'filter4', cssClass
: 'class4', label
: '4', description
: '4' },
1169 { name
: 'filter5', cssClass
: 'class5', label
: '5', description
: '5' },
1170 { name
: 'filter6', label
: '6', description
: '6' }
1173 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1175 model
.initializeFilters( definition
);
1178 !model
.isHighlightEnabled(),
1179 'Initially, highlight is disabled.'
1182 model
.toggleHighlight( true );
1184 model
.isHighlightEnabled(),
1185 'Highlight is enabled on toggle.'
1188 model
.setHighlightColor( 'group1__filter1', 'color1' );
1189 model
.setHighlightColor( 'group1__filter2', 'color2' );
1192 model
.getHighlightedItems().map( function ( item
) {
1193 return item
.getName();
1199 'Highlighted items are highlighted.'
1203 model
.getItemByName( 'group1__filter1' ).getHighlightColor(),
1205 'Item highlight color is set.'
1208 model
.setHighlightColor( 'group1__filter1', 'color1changed' );
1210 model
.getItemByName( 'group1__filter1' ).getHighlightColor(),
1212 'Item highlight color is changed on setHighlightColor.'
1215 model
.clearHighlightColor( 'group1__filter1' );
1217 model
.getHighlightedItems().map( function ( item
) {
1218 return item
.getName();
1223 'Clear highlight from an item results in the item no longer being highlighted.'
1227 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1228 model
.initializeFilters( definition
);
1230 model
.setHighlightColor( 'group1__filter1', 'color1' );
1231 model
.setHighlightColor( 'group1__filter2', 'color2' );
1232 model
.setHighlightColor( 'group1__filter3', 'color3' );
1235 model
.getHighlightedItems().map( function ( item
) {
1236 return item
.getName();
1243 'Even if highlights are not enabled, the items remember their highlight state'
1244 // NOTE: When actually displaying the highlights, the UI checks whether
1245 // highlighting is generally active and then goes over the highlighted
1246 // items. The item models, however, and the view model in general, still
1247 // retains the knowledge about which filters have different colors, so we
1248 // can seamlessly return to the colors the user previously chose if they
1249 // reapply highlights.
1253 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1254 model
.initializeFilters( definition
);
1256 model
.setHighlightColor( 'group1__filter1', 'color1' );
1257 model
.setHighlightColor( 'group1__filter6', 'color6' );
1260 model
.getHighlightedItems().map( function ( item
) {
1261 return item
.getName();
1266 'Items without a specified class identifier are not highlighted.'
1269 }( mediaWiki
, jQuery
) );