2 QUnit
.module( 'mediawiki.rcfilters - FiltersViewModel' );
4 QUnit
.test( 'Setting up filters', function ( assert
) {
8 type
: 'send_unselected_if_any',
11 name
: 'group1filter1',
12 label
: 'Group 1: Filter 1',
13 description
: 'Description of Filter 1 in Group 1'
16 name
: 'group1filter2',
17 label
: 'Group 1: Filter 2',
18 description
: 'Description of Filter 2 in Group 1'
24 type
: 'send_unselected_if_any',
27 name
: 'group2filter1',
28 label
: 'Group 2: Filter 1',
29 description
: 'Description of Filter 1 in Group 2'
32 name
: 'group2filter2',
33 label
: 'Group 2: Filter 2',
34 description
: 'Description of Filter 2 in Group 2'
40 type
: 'string_options',
43 name
: 'group3filter1',
44 label
: 'Group 3: Filter 1',
45 description
: 'Description of Filter 1 in Group 3'
48 name
: 'group3filter2',
49 label
: 'Group 3: Filter 2',
50 description
: 'Description of Filter 2 in Group 3'
55 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
57 model
.initializeFilters( definition
);
60 model
.getItemByName( 'group1filter1' ) instanceof mw
.rcfilters
.dm
.FilterItem
&&
61 model
.getItemByName( 'group1filter2' ) instanceof mw
.rcfilters
.dm
.FilterItem
&&
62 model
.getItemByName( 'group2filter1' ) instanceof mw
.rcfilters
.dm
.FilterItem
&&
63 model
.getItemByName( 'group2filter2' ) instanceof mw
.rcfilters
.dm
.FilterItem
&&
64 model
.getItemByName( 'group3filter1' ) instanceof mw
.rcfilters
.dm
.FilterItem
&&
65 model
.getItemByName( 'group3filter2' ) instanceof mw
.rcfilters
.dm
.FilterItem
,
66 'Filters instantiated and stored correctly'
70 model
.getSelectedState(),
79 'Initial state of filters'
82 model
.updateFilters( {
88 model
.getSelectedState(),
97 'Updating filter states correctly'
101 QUnit
.test( 'Finding matching filters', function ( assert
) {
105 title
: 'Group 1 title',
106 type
: 'send_unselected_if_any',
109 name
: 'group1filter1',
110 label
: 'Group 1: Filter 1',
111 description
: 'Description of Filter 1 in Group 1'
114 name
: 'group1filter2',
115 label
: 'Group 1: Filter 2',
116 description
: 'Description of Filter 2 in Group 1'
121 title
: 'Group 2 title',
122 type
: 'send_unselected_if_any',
125 name
: 'group2filter1',
126 label
: 'Group 2: Filter 1',
127 description
: 'Description of Filter 1 in Group 2'
130 name
: 'group2filter2',
131 label
: 'xGroup 2: Filter 2',
132 description
: 'Description of Filter 2 in Group 2'
141 group1
: [ 'group1filter1', 'group1filter2' ],
142 group2
: [ 'group2filter1' ]
144 reason
: 'Finds filters starting with the query string'
147 query
: 'filter 2 in group',
149 group1
: [ 'group1filter2' ],
150 group2
: [ 'group2filter2' ]
152 reason
: 'Finds filters containing the query string in their description'
157 group1
: [ 'group1filter1', 'group1filter2' ],
158 group2
: [ 'group2filter1', 'group2filter2' ]
160 reason
: 'Finds filters containing the query string in their group title'
163 model
= new mw
.rcfilters
.dm
.FiltersViewModel(),
164 extractNames = function ( matches
) {
166 Object
.keys( matches
).forEach( function ( groupName
) {
167 result
[ groupName
] = matches
[ groupName
].map( function ( item
) {
168 return item
.getName();
174 model
.initializeFilters( definition
);
176 testCases
.forEach( function ( testCase
) {
177 matches
= model
.findMatches( testCase
.query
);
179 extractNames( matches
),
180 testCase
.expectedMatches
,
185 matches
= model
.findMatches( 'foo' );
187 $.isEmptyObject( matches
),
188 'findMatches returns an empty object when no results found'
192 QUnit
.test( 'getParametersFromFilters', function ( assert
) {
196 type
: 'send_unselected_if_any',
200 label
: 'Group 1: Filter 1',
201 description
: 'Description of Filter 1 in Group 1'
205 label
: 'Group 1: Filter 2',
206 description
: 'Description of Filter 2 in Group 1'
210 label
: 'Group 1: Filter 3',
211 description
: 'Description of Filter 3 in Group 1'
217 type
: 'send_unselected_if_any',
221 label
: 'Group 2: Filter 1',
222 description
: 'Description of Filter 1 in Group 2'
226 label
: 'Group 2: Filter 2',
227 description
: 'Description of Filter 2 in Group 2'
231 label
: 'Group 2: Filter 3',
232 description
: 'Description of Filter 3 in Group 2'
238 type
: 'string_options',
243 label
: 'Group 3: Filter 1',
244 description
: 'Description of Filter 1 in Group 3'
248 label
: 'Group 3: Filter 2',
249 description
: 'Description of Filter 2 in Group 3'
253 label
: 'Group 3: Filter 3',
254 description
: 'Description of Filter 3 in Group 3'
259 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
261 model
.initializeFilters( definition
);
263 // Starting with all filters unselected
265 model
.getParametersFromFilters(),
275 'Unselected filters return all parameters falsey or \'all\'.'
279 model
.updateFilters( {
287 // Only one filter in one group
289 model
.getParametersFromFilters(),
291 // Group 1 (one selected, the others are true)
295 // Group 2 (nothing is selected, all false)
301 'One filters in one "send_unselected_if_any" group returns the other parameters truthy.'
305 model
.updateFilters( {
313 // Two selected filters in one group
315 model
.getParametersFromFilters(),
317 // Group 1 (two selected, the others are true)
321 // Group 2 (nothing is selected, all false)
327 'One filters in one "send_unselected_if_any" group returns the other parameters truthy.'
331 model
.updateFilters( {
339 // All filters of the group are selected == this is the same as not selecting any
341 model
.getParametersFromFilters(),
343 // Group 1 (all selected, all false)
347 // Group 2 (nothing is selected, all false)
353 'All filters selected in one "send_unselected_if_any" group returns all parameters falsy.'
356 // Select 1 filter from string_options
357 model
.updateFilters( {
362 // All filters of the group are selected == this is the same as not selecting any
364 model
.getParametersFromFilters(),
366 // Group 1 (all selected, all)
370 // Group 2 (nothing is selected, all false)
376 'One filter selected in "string_option" group returns that filter in the value.'
379 // Select 2 filters from string_options
380 model
.updateFilters( {
385 // All filters of the group are selected == this is the same as not selecting any
387 model
.getParametersFromFilters(),
389 // Group 1 (all selected, all)
393 // Group 2 (nothing is selected, all false)
397 group3
: 'filter7,filter8'
399 'Two filters selected in "string_option" group returns those filters in the value.'
402 // Select 3 filters from string_options
403 model
.updateFilters( {
408 // All filters of the group are selected == this is the same as not selecting any
410 model
.getParametersFromFilters(),
412 // Group 1 (all selected, all)
416 // Group 2 (nothing is selected, all false)
422 'All filters selected in "string_option" group returns \'all\'.'
427 QUnit
.test( 'getFiltersFromParameters', function ( assert
) {
431 type
: 'send_unselected_if_any',
435 label
: 'Show filter 1',
436 description
: 'Description of Filter 1 in Group 1',
441 label
: 'Show filter 2',
442 description
: 'Description of Filter 2 in Group 1'
446 label
: 'Show filter 3',
447 description
: 'Description of Filter 3 in Group 1',
454 type
: 'send_unselected_if_any',
458 label
: 'Show filter 4',
459 description
: 'Description of Filter 1 in Group 2'
463 label
: 'Show filter 5',
464 description
: 'Description of Filter 2 in Group 2',
469 label
: 'Show filter 6',
470 description
: 'Description of Filter 3 in Group 2'
476 type
: 'string_options',
481 label
: 'Group 3: Filter 1',
482 description
: 'Description of Filter 1 in Group 3'
486 label
: 'Group 3: Filter 2',
487 description
: 'Description of Filter 2 in Group 3',
492 label
: 'Group 3: Filter 3',
493 description
: 'Description of Filter 3 in Group 3'
498 defaultFilterRepresentation
= {
499 // Group 1 and 2, "send_unselected_if_any", the values of the filters are "flipped" from the values of the parameters
506 // Group 3, "string_options", default values correspond to parameters and filters
511 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
513 model
.initializeFilters( definition
);
515 // Empty query = only default values
517 model
.getFiltersFromParameters( {} ),
518 defaultFilterRepresentation
,
519 'Empty parameter query results in filters in initial default state'
523 model
.getFiltersFromParameters( {
526 $.extend( {}, defaultFilterRepresentation
, {
527 hidefilter1
: false, // The text is "show filter 1"
528 hidefilter2
: false, // The text is "show filter 2"
529 hidefilter3
: false // The text is "show filter 3"
531 'One truthy parameter in a group whose other parameters are true by default makes the rest of the filters in the group false (unchecked)'
535 model
.getFiltersFromParameters( {
540 $.extend( {}, defaultFilterRepresentation
, {
541 hidefilter1
: false, // The text is "show filter 1"
542 hidefilter2
: false, // The text is "show filter 2"
543 hidefilter3
: false // The text is "show filter 3"
545 'All paremeters in the same \'send_unselected_if_any\' group false is equivalent to none are truthy (checked) in the interface'
548 // The ones above don't update the model, so we have a clean state.
549 // getFiltersFromParameters is stateless; any change is unaffected by the current state
550 // This test is demonstrating wrong usage of the method;
551 // We should be aware that getFiltersFromParameters is stateless,
552 // so each call gives us a filter state that only reflects the query given.
553 // This means that the two calls to updateFilters() below collide.
554 // The result of the first is overridden by the result of the second,
555 // since both get a full state object from getFiltersFromParameters that **only** relates
556 // to the input it receives.
558 model
.getFiltersFromParameters( {
564 model
.getFiltersFromParameters( {
569 // The result here is ignoring the first updateFilters call
570 // We should receive default values + hidefilter6 as false
572 model
.getSelectedState(),
573 $.extend( {}, defaultFilterRepresentation
, {
577 'getFiltersFromParameters does not care about previous or existing state.'
581 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
582 model
.initializeFilters( definition
);
585 model
.getFiltersFromParameters( {
590 model
.getFiltersFromParameters( {
595 // Simulates minor edits being hidden in preferences, then unhidden via URL
598 model
.getSelectedState(),
599 defaultFilterRepresentation
,
600 'After checking and then unchecking a \'send_unselected_if_any\' filter (without touching other filters in that group), results are default'
604 model
.getFiltersFromParameters( {
609 model
.getSelectedState(),
610 $.extend( {}, defaultFilterRepresentation
, {
615 'A \'string_options\' parameter containing 1 value, results in the corresponding filter as checked'
619 model
.getFiltersFromParameters( {
620 group3
: 'filter7,filter8'
624 model
.getSelectedState(),
625 $.extend( {}, defaultFilterRepresentation
, {
630 'A \'string_options\' parameter containing 2 values, results in both corresponding filters as checked'
634 model
.getFiltersFromParameters( {
635 group3
: 'filter7,filter8,filter9'
639 model
.getSelectedState(),
640 $.extend( {}, defaultFilterRepresentation
, {
645 'A \'string_options\' parameter containing all values, results in all filters of the group as unchecked.'
649 model
.getFiltersFromParameters( {
650 group3
: 'filter7,all,filter9'
654 model
.getSelectedState(),
655 $.extend( {}, defaultFilterRepresentation
, {
660 'A \'string_options\' parameter containing the value \'all\', results in all filters of the group as unchecked.'
664 model
.getFiltersFromParameters( {
665 group3
: 'filter7,foo,filter9'
669 model
.getSelectedState(),
670 $.extend( {}, defaultFilterRepresentation
, {
675 'A \'string_options\' parameter containing an invalid value, results in the invalid value ignored and the valid corresponding filters checked.'
679 QUnit
.test( 'sanitizeStringOptionGroup', function ( assert
) {
683 type
: 'string_options',
687 label
: 'Show filter 1',
688 description
: 'Description of Filter 1 in Group 1'
692 label
: 'Show filter 2',
693 description
: 'Description of Filter 2 in Group 1'
697 label
: 'Show filter 3',
698 description
: 'Description of Filter 3 in Group 1'
703 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
705 model
.initializeFilters( definition
);
708 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'filter1', 'filter2' ] ),
709 [ 'filter1', 'filter2' ],
710 'Remove duplicate values'
714 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'foo', 'filter2' ] ),
715 [ 'filter1', 'filter2' ],
716 'Remove invalid values'
720 model
.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'all', 'filter2' ] ),
722 'If any value is "all", the only value is "all".'
726 QUnit
.test( 'setFiltersToDefaults', function ( assert
) {
730 type
: 'send_unselected_if_any',
734 label
: 'Show filter 1',
735 description
: 'Description of Filter 1 in Group 1',
740 label
: 'Show filter 2',
741 description
: 'Description of Filter 2 in Group 1'
745 label
: 'Show filter 3',
746 description
: 'Description of Filter 3 in Group 1',
753 type
: 'send_unselected_if_any',
757 label
: 'Show filter 4',
758 description
: 'Description of Filter 1 in Group 2'
762 label
: 'Show filter 5',
763 description
: 'Description of Filter 2 in Group 2',
768 label
: 'Show filter 6',
769 description
: 'Description of Filter 3 in Group 2'
774 defaultFilterRepresentation
= {
775 // Group 1 and 2, "send_unselected_if_any", the values of the filters are "flipped" from the values of the parameters
783 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
785 model
.initializeFilters( definition
);
788 model
.getSelectedState(),
797 'Initial state: default filters are not selected (controller selects defaults explicitly).'
800 model
.updateFilters( {
805 model
.setFiltersToDefaults();
808 model
.getSelectedState(),
809 defaultFilterRepresentation
,
810 'Changing values of filters and then returning to defaults still results in default filters being selected.'
814 QUnit
.test( 'Filter interaction: subsets', function ( assert
) {
818 type
: 'string_options',
822 label
: 'Show filter 1',
823 description
: 'Description of Filter 1 in Group 1',
824 subset
: [ 'filter2', 'filter5' ]
828 label
: 'Show filter 2',
829 description
: 'Description of Filter 2 in Group 1'
833 label
: 'Show filter 3',
834 description
: 'Description of Filter 3 in Group 1'
840 type
: 'send_unselected_if_any',
844 label
: 'Show filter 4',
845 description
: 'Description of Filter 1 in Group 2',
846 subset
: [ 'filter3', 'filter5' ]
850 label
: 'Show filter 5',
851 description
: 'Description of Filter 2 in Group 2'
855 label
: 'Show filter 6',
856 description
: 'Description of Filter 3 in Group 2'
862 filter1
: { selected
: false, conflicted
: false, included
: false },
863 filter2
: { selected
: false, conflicted
: false, included
: false },
864 filter3
: { selected
: false, conflicted
: false, included
: false },
865 filter4
: { selected
: false, conflicted
: false, included
: false },
866 filter5
: { selected
: false, conflicted
: false, included
: false },
867 filter6
: { selected
: false, conflicted
: false, included
: false }
869 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
871 model
.initializeFilters( definition
);
872 // Select a filter that has subset with another filter
873 model
.updateFilters( {
877 model
.reassessFilterInteractions( model
.getItemByName( 'filter1' ) );
879 model
.getFullState(),
880 $.extend( true, {}, baseFullState
, {
881 filter1
: { selected
: true },
882 filter2
: { included
: true },
883 filter5
: { included
: true }
885 'Filters with subsets are represented in the model.'
888 // Select another filter that has a subset with the same previous filter
889 model
.updateFilters( {
892 model
.reassessFilterInteractions( model
.getItemByName( 'filter4' ) );
894 model
.getFullState(),
895 $.extend( true, {}, baseFullState
, {
896 filter1
: { selected
: true },
897 filter2
: { included
: true },
898 filter3
: { included
: true },
899 filter4
: { selected
: true },
900 filter5
: { included
: true }
902 'Filters that have multiple subsets are represented.'
905 // Remove one filter (but leave the other) that affects filter2
906 model
.updateFilters( {
909 model
.reassessFilterInteractions( model
.getItemByName( 'filter1' ) );
911 model
.getFullState(),
912 $.extend( true, {}, baseFullState
, {
913 filter2
: { included
: false },
914 filter3
: { included
: true },
915 filter4
: { selected
: true },
916 filter5
: { included
: true }
918 'Removing a filter only un-includes its subset if there is no other filter affecting.'
921 model
.updateFilters( {
924 model
.reassessFilterInteractions( model
.getItemByName( 'filter4' ) );
926 model
.getFullState(),
928 'Removing all supersets also un-includes the subsets.'
932 QUnit
.test( 'Filter interaction: full coverage', function ( assert
) {
936 type
: 'string_options',
946 type
: 'send_unselected_if_any',
955 isCapsuleItemMuted = function ( filterName
) {
956 var itemModel
= model
.getItemByName( filterName
),
957 groupModel
= itemModel
.getGroupModel();
959 // This is the logic inside the capsule widget
961 // The capsule item widget only appears if the item is selected
962 itemModel
.isSelected() &&
963 // Muted state is only valid if group is full coverage and all items are selected
964 groupModel
.isFullCoverage() && groupModel
.areAllSelected()
967 getCurrentItemsMutedState = function () {
969 filter1
: isCapsuleItemMuted( 'filter1' ),
970 filter2
: isCapsuleItemMuted( 'filter2' ),
971 filter3
: isCapsuleItemMuted( 'filter3' ),
972 filter4
: isCapsuleItemMuted( 'filter4' ),
973 filter5
: isCapsuleItemMuted( 'filter5' ),
974 filter6
: isCapsuleItemMuted( 'filter6' )
985 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
987 model
.initializeFilters( definition
);
989 // Starting state, no selection, all items are non-muted
991 getCurrentItemsMutedState(),
993 'No selection - all items are non-muted'
996 // Select most (but not all) items in each group
997 model
.updateFilters( {
1004 // Both groups have multiple (but not all) items selected, all items are non-muted
1006 getCurrentItemsMutedState(),
1008 'Not all items in the group selected - all items are non-muted'
1011 // Select all items in 'fullCoverage' group (group2)
1012 model
.updateFilters( {
1016 // Group2 (full coverage) has all items selected, all its items are muted
1018 getCurrentItemsMutedState(),
1019 $.extend( {}, baseMuteState
, {
1024 'All items in \'full coverage\' group are selected - all items in the group are muted'
1027 // Select all items in non 'fullCoverage' group (group1)
1028 model
.updateFilters( {
1032 // Group1 (full coverage) has all items selected, no items in it are muted (non full coverage)
1034 getCurrentItemsMutedState(),
1035 $.extend( {}, baseMuteState
, {
1040 'All items in a non \'full coverage\' group are selected - none of the items in the group are muted'
1043 // Uncheck an item from each group
1044 model
.updateFilters( {
1049 getCurrentItemsMutedState(),
1051 'Not all items in the group are checked - all items are non-muted regardless of group coverage'
1055 QUnit
.test( 'Filter interaction: conflicts', function ( assert
) {
1059 type
: 'string_options',
1063 conflicts
: [ 'filter2', 'filter4' ]
1067 conflicts
: [ 'filter6' ]
1076 type
: 'send_unselected_if_any',
1083 conflicts
: [ 'filter3' ]
1092 filter1
: { selected
: false, conflicted
: false, included
: false },
1093 filter2
: { selected
: false, conflicted
: false, included
: false },
1094 filter3
: { selected
: false, conflicted
: false, included
: false },
1095 filter4
: { selected
: false, conflicted
: false, included
: false },
1096 filter5
: { selected
: false, conflicted
: false, included
: false },
1097 filter6
: { selected
: false, conflicted
: false, included
: false }
1099 model
= new mw
.rcfilters
.dm
.FiltersViewModel();
1101 model
.initializeFilters( definition
);
1104 model
.getFullState(),
1106 'Initial state: no conflicts because no selections.'
1109 // Select a filter that has a conflict with another
1110 model
.updateFilters( {
1111 filter1
: true // conflicts: filter2, filter4
1114 model
.reassessFilterInteractions( model
.getItemByName( 'filter1' ) );
1117 model
.getFullState(),
1118 $.extend( true, {}, baseFullState
, {
1119 filter1
: { selected
: true },
1120 filter2
: { conflicted
: true },
1121 filter4
: { conflicted
: true },
1123 'Selecting a filter set its conflicts list as "conflicted".'
1126 // Select one of the conflicts (both filters are now conflicted and selected)
1127 model
.updateFilters( {
1128 filter4
: true // conflicts: filter 1
1130 model
.reassessFilterInteractions( model
.getItemByName( 'filter4' ) );
1133 model
.getFullState(),
1134 $.extend( true, {}, baseFullState
, {
1135 filter1
: { selected
: true, conflicted
: true },
1136 filter2
: { conflicted
: true },
1137 filter4
: { selected
: true, conflicted
: true },
1139 'Selecting a conflicting filter sets both sides to conflicted and selected.'
1142 // Select another filter from filter4 group, meaning:
1143 // now filter1 no longer conflicts with filter4
1144 model
.updateFilters( {
1145 filter6
: true // conflicts: filter2
1147 model
.reassessFilterInteractions( model
.getItemByName( 'filter6' ) );
1150 model
.getFullState(),
1151 $.extend( true, {}, baseFullState
, {
1152 filter1
: { selected
: true, conflicted
: false }, // No longer conflicts (filter4 is not the only in the group)
1153 filter2
: { conflicted
: true }, // While not selected, still in conflict with filter1, which is selected
1154 filter4
: { selected
: true, conflicted
: false }, // No longer conflicts with filter1
1155 filter6
: { selected
: true, conflicted
: false }
1157 'Selecting a non-conflicting filter from a conflicting group removes the conflict'
1160 }( mediaWiki
, jQuery
) );