diff options
Diffstat (limited to 'www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters')
5 files changed, 2730 insertions, 0 deletions
diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js new file mode 100644 index 00000000..872f4ddf --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js @@ -0,0 +1,354 @@ +/* eslint-disable camelcase */ +/* eslint no-underscore-dangle: "off" */ +( function ( mw, $ ) { + var mockFilterStructure = [ { + name: 'group1', + title: 'Group 1', + type: 'send_unselected_if_any', + filters: [ + { name: 'filter1', cssClass: 'filter1class', default: true }, + { name: 'filter2', cssClass: 'filter2class' } + ] + }, { + name: 'group2', + title: 'Group 2', + type: 'send_unselected_if_any', + filters: [ + { name: 'filter3', cssClass: 'filter3class' }, + { name: 'filter4', cssClass: 'filter4class', default: true } + ] + }, { + name: 'group3', + title: 'Group 3', + type: 'string_options', + filters: [ + { name: 'filter5', cssClass: 'filter5class' }, + { name: 'filter6' } // Not supporting highlights + ] + }, { + name: 'group4', + title: 'Group 4', + type: 'boolean', + sticky: true, + filters: [ + { name: 'stickyFilter7', cssClass: 'filter7class' }, + { name: 'stickyFilter8', cssClass: 'filter8class' } + ] + } ], + minimalDefaultParams = { + filter1: '1', + filter4: '1' + }; + + QUnit.module( 'mediawiki.rcfilters - UriProcessor' ); + + QUnit.test( 'getVersion', function ( assert ) { + var uriProcessor = new mw.rcfilters.UriProcessor( new mw.rcfilters.dm.FiltersViewModel() ); + + assert.ok( + uriProcessor.getVersion( { param1: 'foo', urlversion: '2' } ), + 2, + 'Retrieving the version from the URI query' + ); + + assert.ok( + uriProcessor.getVersion( { param1: 'foo' } ), + 1, + 'Getting version 1 if no version is specified' + ); + } ); + + QUnit.test( 'getUpdatedUri', function ( assert ) { + var uriProcessor, + filtersModel = new mw.rcfilters.dm.FiltersViewModel(), + makeUri = function ( queryParams ) { + var uri = new mw.Uri( 'http://server/wiki/Special:RC' ); + uri.query = queryParams; + return uri; + }; + + filtersModel.initializeFilters( mockFilterStructure ); + uriProcessor = new mw.rcfilters.UriProcessor( filtersModel ); + + assert.deepEqual( + ( uriProcessor.getUpdatedUri( makeUri( {} ) ) ).query, + { urlversion: '2' }, + 'Empty model state with empty uri state, assumes the given uri is already normalized, and adds urlversion=2' + ); + + assert.deepEqual( + ( uriProcessor.getUpdatedUri( makeUri( { foo: 'bar' } ) ) ).query, + { urlversion: '2', foo: 'bar' }, + 'Empty model state with unrecognized params retains unrecognized params' + ); + + // Update the model + filtersModel.toggleFiltersSelected( { + group1__filter1: true, // Param: filter2: '1' + group3__filter5: true // Param: group3: 'filter5' + } ); + + assert.deepEqual( + ( uriProcessor.getUpdatedUri( makeUri( {} ) ) ).query, + { urlversion: '2', filter2: '1', group3: 'filter5' }, + 'Model state is reflected in the updated URI' + ); + + assert.deepEqual( + ( uriProcessor.getUpdatedUri( makeUri( { foo: 'bar' } ) ) ).query, + { urlversion: '2', filter2: '1', group3: 'filter5', foo: 'bar' }, + 'Model state is reflected in the updated URI with existing uri params' + ); + } ); + + QUnit.test( 'updateModelBasedOnQuery', function ( assert ) { + var uriProcessor, + filtersModel = new mw.rcfilters.dm.FiltersViewModel(); + + filtersModel.initializeFilters( mockFilterStructure ); + uriProcessor = new mw.rcfilters.UriProcessor( filtersModel ); + + uriProcessor.updateModelBasedOnQuery( {} ); + assert.deepEqual( + filtersModel.getCurrentParameterState(), + minimalDefaultParams, + 'Version 1: Empty url query sets model to defaults' + ); + + uriProcessor.updateModelBasedOnQuery( { urlversion: '2' } ); + assert.deepEqual( + filtersModel.getCurrentParameterState(), + {}, + 'Version 2: Empty url query sets model to all-false' + ); + + uriProcessor.updateModelBasedOnQuery( { filter1: '1', urlversion: '2' } ); + assert.deepEqual( + filtersModel.getCurrentParameterState(), + $.extend( true, {}, { filter1: '1' } ), + 'Parameters in Uri query set parameter value in the model' + ); + } ); + + QUnit.test( 'isNewState', function ( assert ) { + var uriProcessor, + filtersModel = new mw.rcfilters.dm.FiltersViewModel(), + cases = [ + { + states: { + curr: {}, + new: {} + }, + result: false, + message: 'Empty objects are not new state.' + }, + { + states: { + curr: { filter1: '1' }, + new: { filter1: '0' } + }, + result: true, + message: 'Nulified parameter is a new state' + }, + { + states: { + curr: { filter1: '1' }, + new: { filter1: '1', filter2: '1' } + }, + result: true, + message: 'Added parameters are a new state' + }, + { + states: { + curr: { filter1: '1' }, + new: { filter1: '1', filter2: '0' } + }, + result: false, + message: 'Added null parameters are not a new state (normalizing equals old state)' + }, + { + states: { + curr: { filter1: '1' }, + new: { filter1: '1', foo: 'bar' } + }, + result: true, + message: 'Added unrecognized parameters are a new state' + }, + { + states: { + curr: { filter1: '1', foo: 'bar' }, + new: { filter1: '1', foo: 'baz' } + }, + result: true, + message: 'Changed unrecognized parameters are a new state' + } + ]; + + filtersModel.initializeFilters( mockFilterStructure ); + uriProcessor = new mw.rcfilters.UriProcessor( filtersModel ); + + cases.forEach( function ( testCase ) { + assert.equal( + uriProcessor.isNewState( testCase.states.curr, testCase.states.new ), + testCase.result, + testCase.message + ); + } ); + } ); + + QUnit.test( 'doesQueryContainRecognizedParams', function ( assert ) { + var uriProcessor, + filtersModel = new mw.rcfilters.dm.FiltersViewModel(), + cases = [ + { + query: {}, + result: false, + message: 'Empty query is not valid for load.' + }, + { + query: { highlight: '1' }, + result: false, + message: 'Highlight state alone is not valid for load' + }, + { + query: { urlversion: '2' }, + result: true, + message: 'urlversion=2 state alone is valid for load as an empty state' + }, + { + query: { filter1: '1', foo: 'bar' }, + result: true, + message: 'Existence of recognized parameters makes the query valid for load' + }, + { + query: { foo: 'bar', debug: true }, + result: false, + message: 'Only unrecognized parameters makes the query invalid for load' + } + ]; + + filtersModel.initializeFilters( mockFilterStructure ); + uriProcessor = new mw.rcfilters.UriProcessor( filtersModel ); + + cases.forEach( function ( testCase ) { + assert.equal( + uriProcessor.doesQueryContainRecognizedParams( testCase.query ), + testCase.result, + testCase.message + ); + } ); + } ); + + QUnit.test( '_getNormalizedQueryParams', function ( assert ) { + var uriProcessor, + filtersModel = new mw.rcfilters.dm.FiltersViewModel(), + cases = [ + { + query: {}, + result: $.extend( true, { urlversion: '2' }, minimalDefaultParams ), + message: 'Empty query returns defaults (urlversion 1).' + }, + { + query: { urlversion: '2' }, + result: { urlversion: '2' }, + message: 'Empty query returns empty (urlversion 2)' + }, + { + query: { filter1: '0' }, + result: { urlversion: '2', filter4: '1' }, + message: 'urlversion 1 returns query that overrides defaults' + }, + { + query: { filter3: '1' }, + result: { urlversion: '2', filter1: '1', filter4: '1', filter3: '1' }, + message: 'urlversion 1 with an extra param value returns query that is joined with defaults' + } + ]; + + filtersModel.initializeFilters( mockFilterStructure ); + uriProcessor = new mw.rcfilters.UriProcessor( filtersModel ); + + cases.forEach( function ( testCase ) { + assert.deepEqual( + uriProcessor._getNormalizedQueryParams( testCase.query ), + testCase.result, + testCase.message + ); + } ); + } ); + + QUnit.test( '_normalizeTargetInUri', function ( assert ) { + var cases = [ + { + input: 'http://host/wiki/Special:RecentChangesLinked/Moai', + output: 'http://host/wiki/Special:RecentChangesLinked?target=Moai', + message: 'Target as subpage in path' + }, + { + input: 'http://host/wiki/Special:RecentChangesLinked/Château', + output: 'http://host/wiki/Special:RecentChangesLinked?target=Château', + message: 'Target as subpage in path with special characters' + }, + { + input: 'http://host/wiki/Special:RecentChangesLinked/Moai/Sub1', + output: 'http://host/wiki/Special:RecentChangesLinked?target=Moai/Sub1', + message: 'Target as subpage also has a subpage' + }, + { + input: 'http://host/wiki/Special:RecentChangesLinked/Category:Foo', + output: 'http://host/wiki/Special:RecentChangesLinked?target=Category:Foo', + message: 'Target as subpage in path (with namespace)' + }, + { + input: 'http://host/wiki/Special:RecentChangesLinked/Category:Foo/Bar', + output: 'http://host/wiki/Special:RecentChangesLinked?target=Category:Foo/Bar', + message: 'Target as subpage in path also has a subpage (with namespace)' + }, + { + input: 'http://host/w/index.php?title=Special:RecentChangesLinked/Moai', + output: 'http://host/w/index.php?title=Special:RecentChangesLinked&target=Moai', + message: 'Target as subpage in title param' + }, + { + input: 'http://host/w/index.php?title=Special:RecentChangesLinked/Moai/Sub1', + output: 'http://host/w/index.php?title=Special:RecentChangesLinked&target=Moai/Sub1', + message: 'Target as subpage in title param also has a subpage' + }, + { + input: 'http://host/w/index.php?title=Special:RecentChangesLinked/Category:Foo/Bar', + output: 'http://host/w/index.php?title=Special:RecentChangesLinked&target=Category:Foo/Bar', + message: 'Target as subpage in title param also has a subpage (with namespace)' + }, + { + input: 'http://host/wiki/Special:Watchlist', + output: 'http://host/wiki/Special:Watchlist', + message: 'No target specified' + }, + { + normalizeTarget: false, + input: 'http://host/wiki/Special:RecentChanges/Foo', + output: 'http://host/wiki/Special:RecentChanges/Foo', + message: 'Do not normalize if "normalizeTarget" is false.' + } + ]; + + cases.forEach( function ( testCase ) { + var uriProcessor = new mw.rcfilters.UriProcessor( + null, + { + normalizeTarget: testCase.normalizeTarget === undefined ? + true : testCase.normalizeTarget + } + ); + + assert.equal( + uriProcessor._normalizeTargetInUri( + new mw.Uri( testCase.input ) + ).toString(), + new mw.Uri( testCase.output ).toString(), + testCase.message + ); + } ); + } ); + +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FilterItem.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FilterItem.test.js new file mode 100644 index 00000000..18a2c9ce --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FilterItem.test.js @@ -0,0 +1,205 @@ +/* eslint-disable camelcase */ +( function ( mw ) { + QUnit.module( 'mediawiki.rcfilters - FilterItem' ); + + QUnit.test( 'Initializing filter item', function ( assert ) { + var item, + group1 = new mw.rcfilters.dm.FilterGroup( 'group1' ), + group2 = new mw.rcfilters.dm.FilterGroup( 'group2' ); + + item = new mw.rcfilters.dm.FilterItem( 'filter1', group1 ); + assert.equal( + item.getName(), + 'group1__filter1', + 'Filter name is retained.' + ); + assert.equal( + item.getGroupName(), + 'group1', + 'Group name is retained.' + ); + + item = new mw.rcfilters.dm.FilterItem( + 'filter1', + group1, + { + label: 'test label', + description: 'test description' + } + ); + assert.equal( + item.getLabel(), + 'test label', + 'Label information is retained.' + ); + assert.equal( + item.getLabel(), + 'test label', + 'Description information is retained.' + ); + + item = new mw.rcfilters.dm.FilterItem( + 'filter1', + group1, + { + selected: true + } + ); + assert.equal( + item.isSelected(), + true, + 'Item can be selected in the config.' + ); + item.toggleSelected( true ); + assert.equal( + item.isSelected(), + true, + 'Item can toggle its selected state.' + ); + + // Subsets + item = new mw.rcfilters.dm.FilterItem( + 'filter1', + group1, + { + subset: [ 'sub1', 'sub2', 'sub3' ] + } + ); + assert.deepEqual( + item.getSubset(), + [ 'sub1', 'sub2', 'sub3' ], + 'Subset information is retained.' + ); + assert.equal( + item.existsInSubset( 'sub1' ), + true, + 'Specific item exists in subset.' + ); + assert.equal( + item.existsInSubset( 'sub10' ), + false, + 'Specific item does not exists in subset.' + ); + assert.equal( + item.isIncluded(), + false, + 'Initial state of "included" is false.' + ); + + item.toggleIncluded( true ); + assert.equal( + item.isIncluded(), + true, + 'Item toggles its included state.' + ); + + // Conflicts + item = new mw.rcfilters.dm.FilterItem( + 'filter1', + group1, + { + conflicts: { + group2__conflict1: { group: 'group2', filter: 'group2__conflict1' }, + group2__conflict2: { group: 'group2', filter: 'group2__conflict2' }, + group2__conflict3: { group: 'group2', filter: 'group2__conflict3' } + } + } + ); + assert.deepEqual( + item.getConflicts(), + { + group2__conflict1: { group: 'group2', filter: 'group2__conflict1' }, + group2__conflict2: { group: 'group2', filter: 'group2__conflict2' }, + group2__conflict3: { group: 'group2', filter: 'group2__conflict3' } + }, + 'Conflict information is retained.' + ); + assert.equal( + item.existsInConflicts( new mw.rcfilters.dm.FilterItem( 'conflict1', group2 ) ), + true, + 'Specific item exists in conflicts.' + ); + assert.equal( + item.existsInConflicts( new mw.rcfilters.dm.FilterItem( 'conflict10', group1 ) ), + false, + 'Specific item does not exists in conflicts.' + ); + assert.equal( + item.isConflicted(), + false, + 'Initial state of "conflicted" is false.' + ); + + item.toggleConflicted( true ); + assert.equal( + item.isConflicted(), + true, + 'Item toggles its conflicted state.' + ); + + // Fully covered + item = new mw.rcfilters.dm.FilterItem( 'filter1', group1 ); + assert.equal( + item.isFullyCovered(), + false, + 'Initial state of "full coverage" is false.' + ); + item.toggleFullyCovered( true ); + assert.equal( + item.isFullyCovered(), + true, + 'Item toggles its fully coverage state.' + ); + + } ); + + QUnit.test( 'Emitting events', function ( assert ) { + var group1 = new mw.rcfilters.dm.FilterGroup( 'group1' ), + item = new mw.rcfilters.dm.FilterItem( 'filter1', group1 ), + events = []; + + // Listen to update events + item.on( 'update', function () { + events.push( item.getState() ); + } ); + + // Do stuff + item.toggleSelected( true ); // { selected: true, included: false, conflicted: false, fullyCovered: false } + item.toggleSelected( true ); // No event (duplicate state) + item.toggleIncluded( true ); // { selected: true, included: true, conflicted: false, fullyCovered: false } + item.toggleConflicted( true ); // { selected: true, included: true, conflicted: true, fullyCovered: false } + item.toggleFullyCovered( true ); // { selected: true, included: true, conflicted: true, fullyCovered: true } + item.toggleSelected(); // { selected: false, included: true, conflicted: true, fullyCovered: true } + + // Check emitted events + assert.deepEqual( + events, + [ + { selected: true, included: false, conflicted: false, fullyCovered: false }, + { selected: true, included: true, conflicted: false, fullyCovered: false }, + { selected: true, included: true, conflicted: true, fullyCovered: false }, + { selected: true, included: true, conflicted: true, fullyCovered: true }, + { selected: false, included: true, conflicted: true, fullyCovered: true } + ], + 'Events emitted successfully.' + ); + } ); + + QUnit.test( 'get/set boolean value', function ( assert ) { + var group = new mw.rcfilters.dm.FilterGroup( 'group1', { type: 'boolean' } ), + item = new mw.rcfilters.dm.FilterItem( 'filter1', group ); + + item.setValue( '1' ); + + assert.equal( item.getValue(), true, 'Value is coerced to boolean' ); + } ); + + QUnit.test( 'get/set any value', function ( assert ) { + var group = new mw.rcfilters.dm.FilterGroup( 'group1', { type: 'any_value' } ), + item = new mw.rcfilters.dm.FilterItem( 'filter1', group ); + + item.setValue( '1' ); + + assert.equal( item.getValue(), '1', 'Value is kept as-is' ); + } ); +}( mediaWiki ) ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js new file mode 100644 index 00000000..2b42b5ab --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js @@ -0,0 +1,1562 @@ +/* eslint-disable camelcase */ +( function ( mw, $ ) { + var filterDefinition = [ { + name: 'group1', + type: 'send_unselected_if_any', + filters: [ + { + name: 'filter1', label: 'group1filter1-label', description: 'group1filter1-desc', + default: true, + cssClass: 'filter1class', + conflicts: [ { group: 'group2' } ], + subset: [ + { + group: 'group1', + filter: 'filter2' + }, + { + group: 'group1', + filter: 'filter3' + } + ] + }, + { + name: 'filter2', label: 'group1filter2-label', description: 'group1filter2-desc', + conflicts: [ { group: 'group2', filter: 'filter6' } ], + cssClass: 'filter2class', + subset: [ + { + group: 'group1', + filter: 'filter3' + } + ] + }, + // NOTE: This filter has no highlight! + { name: 'filter3', label: 'group1filter3-label', description: 'group1filter3-desc', default: true } + ] + }, { + name: 'group2', + type: 'send_unselected_if_any', + fullCoverage: true, + conflicts: [ { group: 'group1', filter: 'filter1' } ], + filters: [ + { name: 'filter4', label: 'group2filter4-label', description: 'group2filter4-desc', cssClass: 'filter4class' }, + { name: 'filter5', label: 'group2filter5-label', description: 'group2filter5-desc', default: true, cssClass: 'filter5class' }, + { + name: 'filter6', label: 'group2filter6-label', description: 'group2filter6-desc', cssClass: 'filter6class', + conflicts: [ { group: 'group1', filter: 'filter2' } ] + } + ] + }, { + name: 'group3', + type: 'string_options', + separator: ',', + default: 'filter8', + filters: [ + { name: 'filter7', label: 'group3filter7-label', description: 'group3filter7-desc', cssClass: 'filter7class' }, + { name: 'filter8', label: 'group3filter8-label', description: 'group3filter8-desc', cssClass: 'filter8class' }, + { name: 'filter9', label: 'group3filter9-label', description: 'group3filter9-desc', cssClass: 'filter9class' } + ] + }, { + name: 'group4', + type: 'single_option', + hidden: true, + default: 'option2', + filters: [ + // NOTE: The entire group has no highlight supported + { name: 'option1', label: 'group4option1-label', description: 'group4option1-desc' }, + { name: 'option2', label: 'group4option2-label', description: 'group4option2-desc' }, + { name: 'option3', label: 'group4option3-label', description: 'group4option3-desc' } + ] + }, { + name: 'group5', + type: 'single_option', + filters: [ + { name: 'option1', label: 'group5option1-label', description: 'group5option1-desc', cssClass: 'group5opt1class' }, + { name: 'option2', label: 'group5option2-label', description: 'group5option2-desc', cssClass: 'group5opt2class' }, + { name: 'option3', label: 'group5option3-label', description: 'group5option3-desc', cssClass: 'group5opt3class' } + ] + }, { + name: 'group6', + type: 'boolean', + sticky: true, + filters: [ + { name: 'group6option1', label: 'group6option1-label', description: 'group6option1-desc', cssClass: 'group6opt1class' }, + { name: 'group6option2', label: 'group6option2-label', description: 'group6option2-desc', default: true, cssClass: 'group6opt2class' }, + { name: 'group6option3', label: 'group6option3-label', description: 'group6option3-desc', default: true, cssClass: 'group6opt3class' } + ] + }, { + name: 'group7', + type: 'single_option', + sticky: true, + default: 'group7option2', + filters: [ + { name: 'group7option1', label: 'group7option1-label', description: 'group7option1-desc', cssClass: 'group7opt1class' }, + { name: 'group7option2', label: 'group7option2-label', description: 'group7option2-desc', cssClass: 'group7opt2class' }, + { name: 'group7option3', label: 'group7option3-label', description: 'group7option3-desc', cssClass: 'group7opt3class' } + ] + } ], + shortFilterDefinition = [ { + name: 'group1', + type: 'send_unselected_if_any', + filters: [ { name: 'filter1' }, { name: 'filter2' } ] + }, { + name: 'group2', + type: 'boolean', + hidden: true, + filters: [ { name: 'filter3' }, { name: 'filter4' } ] + }, { + name: 'group3', + type: 'string_options', + sticky: true, + default: 'filter6', + filters: [ { name: 'filter5' }, { name: 'filter6' }, { name: 'filter7' } ] + } ], + viewsDefinition = { + namespaces: { + label: 'Namespaces', + trigger: ':', + groups: [ { + name: 'namespace', + label: 'Namespaces', + type: 'string_options', + separator: ';', + filters: [ + { name: 0, label: 'Main', cssClass: 'namespace-0' }, + { name: 1, label: 'Talk', cssClass: 'namespace-1' }, + { name: 2, label: 'User', cssClass: 'namespace-2' }, + { name: 3, label: 'User talk', cssClass: 'namespace-3' } + ] + } ] + } + }, + defaultParameters = { + filter1: '1', + filter2: '0', + filter3: '1', + filter4: '0', + filter5: '1', + filter6: '0', + group3: 'filter8', + group4: 'option2', + group5: 'option1', + namespace: '' + }, + baseParamRepresentation = { + filter1: '0', + filter2: '0', + filter3: '0', + filter4: '0', + filter5: '0', + filter6: '0', + group3: '', + group4: 'option2', + group5: 'option1', + group6option1: '0', + group6option2: '1', + group6option3: '1', + group7: 'group7option2', + namespace: '' + }, + emptyParamRepresentation = { + filter1: '0', + filter2: '0', + filter3: '0', + filter4: '0', + filter5: '0', + filter6: '0', + group3: '', + group4: '', + group5: '', + group6option1: '0', + group6option2: '0', + group6option3: '0', + group7: '', + namespace: '', + // Null highlights + group1__filter1_color: null, + group1__filter2_color: null, + // group1__filter3_color: null, // Highlight isn't supported + group2__filter4_color: null, + group2__filter5_color: null, + group2__filter6_color: null, + group3__filter7_color: null, + group3__filter8_color: null, + group3__filter9_color: null, + // group4__option1_color: null, // Highlight isn't supported + // group4__option2_color: null, // Highlight isn't supported + // group4__option3_color: null, // Highlight isn't supported + group5__option1_color: null, + group5__option2_color: null, + group5__option3_color: null, + group6__group6option1_color: null, + group6__group6option2_color: null, + group6__group6option3_color: null, + group7__group7option1_color: null, + group7__group7option2_color: null, + group7__group7option3_color: null, + namespace__0_color: null, + namespace__1_color: null, + namespace__2_color: null, + namespace__3_color: null + }, + baseFilterRepresentation = { + group1__filter1: false, + group1__filter2: false, + group1__filter3: false, + group2__filter4: false, + group2__filter5: false, + group2__filter6: false, + group3__filter7: false, + group3__filter8: false, + group3__filter9: false, + // The 'single_value' type of group can't have empty value; it's either + // the default given or the first item that will get the truthy value + group4__option1: false, + group4__option2: true, // Default + group4__option3: false, + group5__option1: true, // No default set, first item is default value + group5__option2: false, + group5__option3: false, + group6__group6option1: false, + group6__group6option2: true, + group6__group6option3: true, + group7__group7option1: false, + group7__group7option2: true, + group7__group7option3: false, + namespace__0: false, + namespace__1: false, + namespace__2: false, + namespace__3: false + }, + baseFullFilterState = { + group1__filter1: { selected: false, conflicted: false, included: false }, + group1__filter2: { selected: false, conflicted: false, included: false }, + group1__filter3: { selected: false, conflicted: false, included: false }, + group2__filter4: { selected: false, conflicted: false, included: false }, + group2__filter5: { selected: false, conflicted: false, included: false }, + group2__filter6: { selected: false, conflicted: false, included: false }, + group3__filter7: { selected: false, conflicted: false, included: false }, + group3__filter8: { selected: false, conflicted: false, included: false }, + group3__filter9: { selected: false, conflicted: false, included: false }, + group4__option1: { selected: false, conflicted: false, included: false }, + group4__option2: { selected: true, conflicted: false, included: false }, + group4__option3: { selected: false, conflicted: false, included: false }, + group5__option1: { selected: true, conflicted: false, included: false }, + group5__option2: { selected: false, conflicted: false, included: false }, + group5__option3: { selected: false, conflicted: false, included: false }, + group6__group6option1: { selected: false, conflicted: false, included: false }, + group6__group6option2: { selected: true, conflicted: false, included: false }, + group6__group6option3: { selected: true, conflicted: false, included: false }, + group7__group7option1: { selected: false, conflicted: false, included: false }, + group7__group7option2: { selected: true, conflicted: false, included: false }, + group7__group7option3: { selected: false, conflicted: false, included: false }, + namespace__0: { selected: false, conflicted: false, included: false }, + namespace__1: { selected: false, conflicted: false, included: false }, + namespace__2: { selected: false, conflicted: false, included: false }, + namespace__3: { selected: false, conflicted: false, included: false } + }; + + QUnit.module( 'mediawiki.rcfilters - FiltersViewModel', QUnit.newMwEnvironment( { + messages: { + 'group1filter1-label': 'Group 1: Filter 1 title', + 'group1filter1-desc': 'Description of Filter 1 in Group 1', + 'group1filter2-label': 'Group 1: Filter 2 title', + 'group1filter2-desc': 'Description of Filter 2 in Group 1', + 'group1filter3-label': 'Group 1: Filter 3', + 'group1filter3-desc': 'Description of Filter 3 in Group 1', + + 'group2filter4-label': 'Group 2: Filter 4 title', + 'group2filter4-desc': 'Description of Filter 4 in Group 2', + 'group2filter5-label': 'Group 2: Filter 5', + 'group2filter5-desc': 'Description of Filter 5 in Group 2', + 'group2filter6-label': 'xGroup 2: Filter 6', + 'group2filter6-desc': 'Description of Filter 6 in Group 2' + } + } ) ); + + QUnit.test( 'Setting up filters', function ( assert ) { + var model = new mw.rcfilters.dm.FiltersViewModel(); + + model.initializeFilters( filterDefinition, viewsDefinition ); + + // Test that all items were created + assert.ok( + Object.keys( baseFilterRepresentation ).every( function ( filterName ) { + return model.getItemByName( filterName ) instanceof mw.rcfilters.dm.FilterItem; + } ), + 'Filters instantiated and stored correctly' + ); + + assert.deepEqual( + model.getSelectedState(), + baseFilterRepresentation, + 'Initial state of filters' + ); + + model.toggleFiltersSelected( { + group1__filter1: true, + group2__filter5: true, + group3__filter7: true + } ); + assert.deepEqual( + model.getSelectedState(), + $.extend( true, {}, baseFilterRepresentation, { + group1__filter1: true, + group2__filter5: true, + group3__filter7: true + } ), + 'Updating filter states correctly' + ); + } ); + + QUnit.test( 'Default filters', function ( assert ) { + var model = new mw.rcfilters.dm.FiltersViewModel(); + + model.initializeFilters( filterDefinition, viewsDefinition ); + + // Empty query = only default values + assert.deepEqual( + model.getDefaultParams(), + defaultParameters, + 'Default parameters are stored properly per filter and group (sticky groups are ignored)' + ); + } ); + + QUnit.test( 'Parameter minimal state', function ( assert ) { + var model = new mw.rcfilters.dm.FiltersViewModel(), + cases = [ + { + input: {}, + result: {}, + msg: 'Empty parameter representation produces an empty result' + }, + { + input: { + filter1: '1', + filter2: '0', + filter3: '0', + group3: '', + group4: 'option2' + }, + result: { + filter1: '1', + group4: 'option2' + }, + msg: 'Mixed input results in only non-falsey values as result' + }, + { + input: { + filter1: '0', + filter2: '0', + filter3: '0', + group3: '', + group4: '', + group1__filter1_color: null + }, + result: {}, + msg: 'An all-falsey input results in an empty result.' + }, + { + input: { + filter1: '0', + filter2: '0', + filter3: '0', + group3: '', + group4: '', + group1__filter1_color: 'c1' + }, + result: { + group1__filter1_color: 'c1' + }, + msg: 'An all-falsey input with highlight params result in only the highlight param.' + }, + { + input: { + group1__filter1_color: 'c1', + group1__filter3_color: 'c3' // Not supporting highlights + }, + result: { + group1__filter1_color: 'c1' + }, + msg: 'Unsupported highlights are removed.' + } + ]; + + model.initializeFilters( filterDefinition, viewsDefinition ); + + cases.forEach( function ( test ) { + assert.deepEqual( + model.getMinimizedParamRepresentation( test.input ), + test.result, + test.msg + ); + } ); + } ); + + QUnit.test( 'Parameter states', function ( assert ) { + // Some groups / params have their defaults immediately applied + // to their state. These include single_option which can never + // be empty, etc. These are these states: + var parametersWithoutExcluded, + appliedDefaultParameters = { + group4: 'option2', + group5: 'option1', + // Sticky, their defaults apply immediately + group6option2: '1', + group6option3: '1', + group7: 'group7option2' + }, + model = new mw.rcfilters.dm.FiltersViewModel(); + + model.initializeFilters( filterDefinition, viewsDefinition ); + assert.deepEqual( + model.getEmptyParameterState(), + emptyParamRepresentation, + 'Producing an empty parameter state' + ); + + model.toggleFiltersSelected( { + group1__filter1: true, + group3__filter7: true + } ); + + assert.deepEqual( + model.getCurrentParameterState(), + // appliedDefaultParams applies the default value to parameters + // who must have an initial value to begin with, so we have to + // take it into account in the current state + $.extend( true, {}, appliedDefaultParameters, { + filter2: '1', + filter3: '1', + group3: 'filter7' + } ), + 'Producing a current parameter state' + ); + + // Reset + model = new mw.rcfilters.dm.FiltersViewModel(); + model.initializeFilters( filterDefinition, viewsDefinition ); + + parametersWithoutExcluded = $.extend( true, {}, appliedDefaultParameters ); + delete parametersWithoutExcluded.group7; + delete parametersWithoutExcluded.group6option2; + delete parametersWithoutExcluded.group6option3; + + assert.deepEqual( + model.getCurrentParameterState( true ), + parametersWithoutExcluded, + 'Producing a current clean parameter state without excluded filters' + ); + } ); + + QUnit.test( 'Cleaning up parameter states', function ( assert ) { + var model = new mw.rcfilters.dm.FiltersViewModel(), + cases = [ + { + input: {}, + result: {}, + msg: 'Empty parameter representation produces an empty result' + }, + { + input: { + filter1: '1', // Regular (do not strip) + group6option1: '1' // Sticky + }, + result: { filter1: '1' }, + msg: 'Valid input strips all sticky params regardless of value' + } + ]; + + model.initializeFilters( filterDefinition, viewsDefinition ); + + cases.forEach( function ( test ) { + assert.deepEqual( + model.removeStickyParams( test.input ), + test.result, + test.msg + ); + } ); + + } ); + + QUnit.test( 'Finding matching filters', function ( assert ) { + var matches, + testCases = [ + { + query: 'group', + expectedMatches: { + group1: [ 'group1__filter1', 'group1__filter2', 'group1__filter3' ], + group2: [ 'group2__filter4', 'group2__filter5' ] + }, + reason: 'Finds filters starting with the query string' + }, + { + query: 'in Group 2', + expectedMatches: { + group2: [ 'group2__filter4', 'group2__filter5', 'group2__filter6' ] + }, + reason: 'Finds filters containing the query string in their description' + }, + { + query: 'title', + expectedMatches: { + group1: [ 'group1__filter1', 'group1__filter2' ], + group2: [ 'group2__filter4' ] + }, + reason: 'Finds filters containing the query string in their group title' + }, + { + query: ':Main', + expectedMatches: { + namespace: [ 'namespace__0' ] + }, + reason: 'Finds item in view when a prefix is used' + }, + { + query: ':group', + expectedMatches: {}, + reason: 'Finds no results if using namespaces prefix (:) to search for filter title' + } + ], + model = new mw.rcfilters.dm.FiltersViewModel(), + extractNames = function ( matches ) { + var result = {}; + Object.keys( matches ).forEach( function ( groupName ) { + result[ groupName ] = matches[ groupName ].map( function ( item ) { + return item.getName(); + } ); + } ); + return result; + }; + + model.initializeFilters( filterDefinition, viewsDefinition ); + + testCases.forEach( function ( testCase ) { + matches = model.findMatches( testCase.query ); + assert.deepEqual( + extractNames( matches ), + testCase.expectedMatches, + testCase.reason + ); + } ); + + matches = model.findMatches( 'foo' ); + assert.ok( + $.isEmptyObject( matches ), + 'findMatches returns an empty object when no results found' + ); + } ); + + QUnit.test( 'getParametersFromFilters', function ( assert ) { + var model = new mw.rcfilters.dm.FiltersViewModel(); + + model.initializeFilters( filterDefinition, viewsDefinition ); + + // Starting with all filters unselected + assert.deepEqual( + model.getParametersFromFilters(), + baseParamRepresentation, + 'Unselected filters return all parameters falsey or \'\'.' + ); + + // Select 1 filter + model.toggleFiltersSelected( { + group1__filter1: true + } ); + // Only one filter in one group + assert.deepEqual( + model.getParametersFromFilters(), + $.extend( true, {}, baseParamRepresentation, { + // Group 1 (one selected, the others are true) + filter2: '1', + filter3: '1' + } ), + 'One filter in one "send_unselected_if_any" group returns the other parameters truthy.' + ); + + // Select 2 filters + model.toggleFiltersSelected( { + group1__filter1: true, + group1__filter2: true + } ); + // Two selected filters in one group + assert.deepEqual( + model.getParametersFromFilters(), + $.extend( true, {}, baseParamRepresentation, { + // Group 1 (two selected, the other is true) + filter3: '1' + } ), + 'Two filters in one "send_unselected_if_any" group returns the other parameters truthy.' + ); + + // Select 3 filters + model.toggleFiltersSelected( { + group1__filter1: true, + group1__filter2: true, + group1__filter3: true + } ); + // All filters of the group are selected == this is the same as not selecting any + assert.deepEqual( + model.getParametersFromFilters(), + baseParamRepresentation, + 'All filters selected in one "send_unselected_if_any" group returns all parameters falsy.' + ); + + // Select 1 filter from string_options + model.toggleFiltersSelected( { + group3__filter7: true, + group3__filter8: false, + group3__filter9: false + } ); + // All filters of the group are selected == this is the same as not selecting any + assert.deepEqual( + model.getParametersFromFilters(), + $.extend( true, {}, baseParamRepresentation, { + group3: 'filter7' + } ), + 'One filter selected in "string_option" group returns that filter in the value.' + ); + + // Select 2 filters from string_options + model.toggleFiltersSelected( { + group3__filter7: true, + group3__filter8: true, + group3__filter9: false + } ); + // All filters of the group are selected == this is the same as not selecting any + assert.deepEqual( + model.getParametersFromFilters(), + $.extend( true, {}, baseParamRepresentation, { + group3: 'filter7,filter8' + } ), + 'Two filters selected in "string_option" group returns those filters in the value.' + ); + + // Select 3 filters from string_options + model.toggleFiltersSelected( { + group3__filter7: true, + group3__filter8: true, + group3__filter9: true + } ); + // All filters of the group are selected == this is the same as not selecting any + assert.deepEqual( + model.getParametersFromFilters(), + $.extend( true, {}, baseParamRepresentation, { + group3: 'all' + } ), + 'All filters selected in "string_option" group returns \'all\'.' + ); + + // Reset + model = new mw.rcfilters.dm.FiltersViewModel(); + model.initializeFilters( filterDefinition, viewsDefinition ); + + // Select an option from single_option group + model.toggleFiltersSelected( { + group4__option2: true + } ); + // All filters of the group are selected == this is the same as not selecting any + assert.deepEqual( + model.getParametersFromFilters(), + $.extend( true, {}, baseParamRepresentation, { + group4: 'option2' + } ), + 'Selecting an option from "single_option" group returns that option as a value.' + ); + + // Select a different option from single_option group + model.toggleFiltersSelected( { + group4__option3: true + } ); + // All filters of the group are selected == this is the same as not selecting any + assert.deepEqual( + model.getParametersFromFilters(), + $.extend( true, {}, baseParamRepresentation, { + group4: 'option3' + } ), + 'Selecting a different option from "single_option" group changes the selection.' + ); + } ); + + QUnit.test( 'getParametersFromFilters (custom object)', function ( assert ) { + // This entire test uses different base definition than the global one + // on purpose, to verify that the values inserted as a custom object + // are the ones we expect in return + var originalState, + model = new mw.rcfilters.dm.FiltersViewModel(), + definition = [ { + name: 'group1', + title: 'Group 1', + type: 'send_unselected_if_any', + filters: [ + { name: 'hidefilter1', label: 'Hide filter 1', description: '' }, + { name: 'hidefilter2', label: 'Hide filter 2', description: '' }, + { name: 'hidefilter3', label: 'Hide filter 3', description: '' } + ] + }, { + name: 'group2', + title: 'Group 2', + type: 'send_unselected_if_any', + filters: [ + { name: 'hidefilter4', label: 'Hide filter 4', description: '' }, + { name: 'hidefilter5', label: 'Hide filter 5', description: '' }, + { name: 'hidefilter6', label: 'Hide filter 6', description: '' } + ] + }, { + name: 'group3', + title: 'Group 3', + type: 'string_options', + separator: ',', + filters: [ + { name: 'filter7', label: 'Hide filter 7', description: '' }, + { name: 'filter8', label: 'Hide filter 8', description: '' }, + { name: 'filter9', label: 'Hide filter 9', description: '' } + ] + }, { + name: 'group4', + title: 'Group 4', + type: 'single_option', + filters: [ + { name: 'filter10', label: 'Hide filter 10', description: '' }, + { name: 'filter11', label: 'Hide filter 11', description: '' }, + { name: 'filter12', label: 'Hide filter 12', description: '' } + ] + } ], + baseResult = { + hidefilter1: '0', + hidefilter2: '0', + hidefilter3: '0', + hidefilter4: '0', + hidefilter5: '0', + hidefilter6: '0', + group3: '', + group4: '' + }, + cases = [ + { + // This is mocking the cases above, both + // - 'Two filters in one "send_unselected_if_any" group returns the other parameters truthy.' + // - 'Two filters selected in "string_option" group returns those filters in the value.' + input: { + group1__hidefilter1: true, + group1__hidefilter2: true, + group1__hidefilter3: false, + group2__hidefilter4: false, + group2__hidefilter5: false, + group2__hidefilter6: false, + group3__filter7: true, + group3__filter8: true, + group3__filter9: false + }, + expected: $.extend( true, {}, baseResult, { + // Group 1 (two selected, the others are true) + hidefilter3: '1', + // Group 3 (two selected) + group3: 'filter7,filter8' + } ), + msg: 'Given an explicit (complete) filter state object, the result is the same as if the object given represented the model state.' + }, + { + // This is mocking case above + // - 'One filter in one "send_unselected_if_any" group returns the other parameters truthy.' + input: { + group1__hidefilter1: 1 + }, + expected: $.extend( true, {}, baseResult, { + // Group 1 (one selected, the others are true) + hidefilter2: '1', + hidefilter3: '1' + } ), + msg: 'Given an explicit (incomplete) filter state object, the result is the same as if the object give represented the model state.' + }, + { + input: { + group4__filter10: true + }, + expected: $.extend( true, {}, baseResult, { + group4: 'filter10' + } ), + msg: 'Given a single value for "single_option" that option is represented in the result.' + }, + { + input: { + group4__filter10: true, + group4__filter11: true + }, + expected: $.extend( true, {}, baseResult, { + group4: 'filter10' + } ), + msg: 'Given more than one true value for "single_option" (which should not happen!) only the first value counts, and the second is ignored.' + }, + { + input: {}, + expected: baseResult, + msg: 'Given an explicit empty object, the result is all filters set to their falsey unselected value.' + } + ]; + + model.initializeFilters( definition ); + // Store original state + originalState = model.getSelectedState(); + + // Test each case + cases.forEach( function ( test ) { + assert.deepEqual( + model.getParametersFromFilters( test.input ), + test.expected, + test.msg + ); + } ); + + // After doing the above tests, make sure the actual state + // of the filter stayed the same + assert.deepEqual( + model.getSelectedState(), + originalState, + 'Running the method with external definition to parse does not actually change the state of the model' + ); + } ); + + QUnit.test( 'getFiltersFromParameters', function ( assert ) { + var model = new mw.rcfilters.dm.FiltersViewModel(); + + model.initializeFilters( filterDefinition, viewsDefinition ); + + // Empty query = only default values + assert.deepEqual( + model.getFiltersFromParameters( {} ), + baseFilterRepresentation, + 'Empty parameter query results in an object representing all filters set to their base state' + ); + + assert.deepEqual( + model.getFiltersFromParameters( { + filter2: '1' + } ), + $.extend( {}, baseFilterRepresentation, { + group1__filter1: true, // The text is "show filter 1" + group1__filter2: false, // The text is "show filter 2" + group1__filter3: true // The text is "show filter 3" + } ), + 'One truthy parameter in a group whose other parameters are true by default makes the rest of the filters in the group false (unchecked)' + ); + + assert.deepEqual( + model.getFiltersFromParameters( { + filter1: '1', + filter2: '1', + filter3: '1' + } ), + $.extend( {}, baseFilterRepresentation, { + group1__filter1: false, // The text is "show filter 1" + group1__filter2: false, // The text is "show filter 2" + group1__filter3: false // The text is "show filter 3" + } ), + 'All paremeters in the same \'send_unselected_if_any\' group false is equivalent to none are truthy (checked) in the interface' + ); + + // The ones above don't update the model, so we have a clean state. + // getFiltersFromParameters is stateless; any change is unaffected by the current state + // This test is demonstrating wrong usage of the method; + // We should be aware that getFiltersFromParameters is stateless, + // so each call gives us a filter state that only reflects the query given. + // This means that the two calls to toggleFiltersSelected() below collide. + // The result of the first is overridden by the result of the second, + // since both get a full state object from getFiltersFromParameters that **only** relates + // to the input it receives. + model.toggleFiltersSelected( + model.getFiltersFromParameters( { + filter1: '1' + } ) + ); + + model.toggleFiltersSelected( + model.getFiltersFromParameters( { + filter6: '1' + } ) + ); + + // The result here is ignoring the first toggleFiltersSelected call + assert.deepEqual( + model.getSelectedState(), + $.extend( {}, baseFilterRepresentation, { + group2__filter4: true, + group2__filter5: true, + group2__filter6: false + } ), + 'getFiltersFromParameters does not care about previous or existing state.' + ); + + // Reset + model = new mw.rcfilters.dm.FiltersViewModel(); + model.initializeFilters( filterDefinition, viewsDefinition ); + + model.toggleFiltersSelected( + model.getFiltersFromParameters( { + group3: 'filter7' + } ) + ); + assert.deepEqual( + model.getSelectedState(), + $.extend( {}, baseFilterRepresentation, { + group3__filter7: true, + group3__filter8: false, + group3__filter9: false + } ), + 'A \'string_options\' parameter containing 1 value, results in the corresponding filter as checked' + ); + + model.toggleFiltersSelected( + model.getFiltersFromParameters( { + group3: 'filter7,filter8' + } ) + ); + assert.deepEqual( + model.getSelectedState(), + $.extend( {}, baseFilterRepresentation, { + group3__filter7: true, + group3__filter8: true, + group3__filter9: false + } ), + 'A \'string_options\' parameter containing 2 values, results in both corresponding filters as checked' + ); + + model.toggleFiltersSelected( + model.getFiltersFromParameters( { + group3: 'filter7,filter8,filter9' + } ) + ); + assert.deepEqual( + model.getSelectedState(), + $.extend( {}, baseFilterRepresentation, { + group3__filter7: true, + group3__filter8: true, + group3__filter9: true + } ), + 'A \'string_options\' parameter containing all values, results in all filters of the group as checked.' + ); + + model.toggleFiltersSelected( + model.getFiltersFromParameters( { + group3: 'filter7,all,filter9' + } ) + ); + assert.deepEqual( + model.getSelectedState(), + $.extend( {}, baseFilterRepresentation, { + group3__filter7: true, + group3__filter8: true, + group3__filter9: true + } ), + 'A \'string_options\' parameter containing the value \'all\', results in all filters of the group as checked.' + ); + + model.toggleFiltersSelected( + model.getFiltersFromParameters( { + group3: 'filter7,foo,filter9' + } ) + ); + assert.deepEqual( + model.getSelectedState(), + $.extend( {}, baseFilterRepresentation, { + group3__filter7: true, + group3__filter8: false, + group3__filter9: true + } ), + 'A \'string_options\' parameter containing an invalid value, results in the invalid value ignored and the valid corresponding filters checked.' + ); + + model.toggleFiltersSelected( + model.getFiltersFromParameters( { + group4: 'option1' + } ) + ); + assert.deepEqual( + model.getSelectedState(), + $.extend( {}, baseFilterRepresentation, { + group4__option1: true, + group4__option2: false + } ), + 'A \'single_option\' parameter reflects a single selected value.' + ); + + assert.deepEqual( + model.getFiltersFromParameters( { + group4: 'option1,option2' + } ), + baseFilterRepresentation, + 'An invalid \'single_option\' parameter is ignored.' + ); + + // Change to one value + model.toggleFiltersSelected( + model.getFiltersFromParameters( { + group4: 'option1' + } ) + ); + // Change again to another value + model.toggleFiltersSelected( + model.getFiltersFromParameters( { + group4: 'option2' + } ) + ); + assert.deepEqual( + model.getSelectedState(), + $.extend( {}, baseFilterRepresentation, { + group4__option2: true + } ), + 'A \'single_option\' parameter always reflects the latest selected value.' + ); + } ); + + QUnit.test( 'sanitizeStringOptionGroup', function ( assert ) { + var model = new mw.rcfilters.dm.FiltersViewModel(); + + model.initializeFilters( filterDefinition, viewsDefinition ); + + assert.deepEqual( + model.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'filter1', 'filter2' ] ), + [ 'filter1', 'filter2' ], + 'Remove duplicate values' + ); + + assert.deepEqual( + model.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'foo', 'filter2' ] ), + [ 'filter1', 'filter2' ], + 'Remove invalid values' + ); + + assert.deepEqual( + model.sanitizeStringOptionGroup( 'group1', [ 'filter1', 'all', 'filter2' ] ), + [ 'all' ], + 'If any value is "all", the only value is "all".' + ); + } ); + + QUnit.test( 'Filter interaction: subsets', function ( assert ) { + var model = new mw.rcfilters.dm.FiltersViewModel(); + + model.initializeFilters( filterDefinition, viewsDefinition ); + + // Select a filter that has subset with another filter + model.toggleFiltersSelected( { + group1__filter1: true + } ); + + model.reassessFilterInteractions( model.getItemByName( 'group1__filter1' ) ); + assert.deepEqual( + model.getFullState(), + $.extend( true, {}, baseFullFilterState, { + group1__filter1: { selected: true }, + group1__filter2: { included: true }, + group1__filter3: { included: true }, + // Conflicts are affected + group2__filter4: { conflicted: true }, + group2__filter5: { conflicted: true }, + group2__filter6: { conflicted: true } + } ), + 'Filters with subsets are represented in the model.' + ); + + // Select another filter that has a subset with the same previous filter + model.toggleFiltersSelected( { + group1__filter2: true + } ); + model.reassessFilterInteractions( model.getItemByName( 'filter2' ) ); + assert.deepEqual( + model.getFullState(), + $.extend( true, {}, baseFullFilterState, { + group1__filter1: { selected: true }, + group1__filter2: { selected: true, included: true }, + group1__filter3: { included: true }, + // Conflicts are affected + group2__filter6: { conflicted: true } + } ), + 'Filters that have multiple subsets are represented.' + ); + + // Remove one filter (but leave the other) that affects filter3 + model.toggleFiltersSelected( { + group1__filter1: false + } ); + model.reassessFilterInteractions( model.getItemByName( 'group1__filter1' ) ); + assert.deepEqual( + model.getFullState(), + $.extend( true, {}, baseFullFilterState, { + group1__filter2: { selected: true, included: false }, + group1__filter3: { included: true }, + // Conflicts are affected + group2__filter6: { conflicted: true } + } ), + 'Removing a filter only un-includes its subset if there is no other filter affecting.' + ); + + model.toggleFiltersSelected( { + group1__filter2: false + } ); + model.reassessFilterInteractions( model.getItemByName( 'group1__filter2' ) ); + assert.deepEqual( + model.getFullState(), + baseFullFilterState, + 'Removing all supersets also un-includes the subsets.' + ); + } ); + + QUnit.test( 'Filter interaction: full coverage', function ( assert ) { + var model = new mw.rcfilters.dm.FiltersViewModel(), + isCapsuleItemMuted = function ( filterName ) { + var itemModel = model.getItemByName( filterName ), + groupModel = itemModel.getGroupModel(); + + // This is the logic inside the capsule widget + return ( + // The capsule item widget only appears if the item is selected + itemModel.isSelected() && + // Muted state is only valid if group is full coverage and all items are selected + groupModel.isFullCoverage() && groupModel.areAllSelected() + ); + }, + getCurrentItemsMutedState = function () { + return { + group1__filter1: isCapsuleItemMuted( 'group1__filter1' ), + group1__filter2: isCapsuleItemMuted( 'group1__filter2' ), + group1__filter3: isCapsuleItemMuted( 'group1__filter3' ), + group2__filter4: isCapsuleItemMuted( 'group2__filter4' ), + group2__filter5: isCapsuleItemMuted( 'group2__filter5' ), + group2__filter6: isCapsuleItemMuted( 'group2__filter6' ) + }; + }, + baseMuteState = { + group1__filter1: false, + group1__filter2: false, + group1__filter3: false, + group2__filter4: false, + group2__filter5: false, + group2__filter6: false + }; + + model.initializeFilters( filterDefinition, viewsDefinition ); + + // Starting state, no selection, all items are non-muted + assert.deepEqual( + getCurrentItemsMutedState(), + baseMuteState, + 'No selection - all items are non-muted' + ); + + // Select most (but not all) items in each group + model.toggleFiltersSelected( { + group1__filter1: true, + group1__filter2: true, + group2__filter4: true, + group2__filter5: true + } ); + + // Both groups have multiple (but not all) items selected, all items are non-muted + assert.deepEqual( + getCurrentItemsMutedState(), + baseMuteState, + 'Not all items in the group selected - all items are non-muted' + ); + + // Select all items in 'fullCoverage' group (group2) + model.toggleFiltersSelected( { + group2__filter6: true + } ); + + // Group2 (full coverage) has all items selected, all its items are muted + assert.deepEqual( + getCurrentItemsMutedState(), + $.extend( {}, baseMuteState, { + group2__filter4: true, + group2__filter5: true, + group2__filter6: true + } ), + 'All items in \'full coverage\' group are selected - all items in the group are muted' + ); + + // Select all items in non 'fullCoverage' group (group1) + model.toggleFiltersSelected( { + group1__filter3: true + } ); + + // Group1 (full coverage) has all items selected, no items in it are muted (non full coverage) + assert.deepEqual( + getCurrentItemsMutedState(), + $.extend( {}, baseMuteState, { + group2__filter4: true, + group2__filter5: true, + group2__filter6: true + } ), + 'All items in a non \'full coverage\' group are selected - none of the items in the group are muted' + ); + + // Uncheck an item from each group + model.toggleFiltersSelected( { + group1__filter3: false, + group2__filter5: false + } ); + assert.deepEqual( + getCurrentItemsMutedState(), + baseMuteState, + 'Not all items in the group are checked - all items are non-muted regardless of group coverage' + ); + } ); + + QUnit.test( 'Filter interaction: conflicts', function ( assert ) { + var model = new mw.rcfilters.dm.FiltersViewModel(); + + model.initializeFilters( filterDefinition, viewsDefinition ); + + assert.deepEqual( + model.getFullState(), + baseFullFilterState, + 'Initial state: no conflicts because no selections.' + ); + + // Select a filter that has a conflict with an entire group + model.toggleFiltersSelected( { + group1__filter1: true // conflicts: entire of group 2 ( filter4, filter5, filter6) + } ); + + model.reassessFilterInteractions( model.getItemByName( 'group1__filter1' ) ); + + assert.deepEqual( + model.getFullState(), + $.extend( true, {}, baseFullFilterState, { + group1__filter1: { selected: true }, + group2__filter4: { conflicted: true }, + group2__filter5: { conflicted: true }, + group2__filter6: { conflicted: true }, + // Subsets are affected by the selection + group1__filter2: { included: true }, + group1__filter3: { included: true } + } ), + 'Selecting a filter that conflicts with a group sets all the conflicted group items as "conflicted".' + ); + + // Select one of the conflicts (both filters are now conflicted and selected) + model.toggleFiltersSelected( { + group2__filter4: true // conflicts: filter 1 + } ); + model.reassessFilterInteractions( model.getItemByName( 'group2__filter4' ) ); + + assert.deepEqual( + model.getFullState(), + $.extend( true, {}, baseFullFilterState, { + group1__filter1: { selected: true, conflicted: true }, + group2__filter4: { selected: true, conflicted: true }, + group2__filter5: { conflicted: true }, + group2__filter6: { conflicted: true }, + // Subsets are affected by the selection + group1__filter2: { included: true }, + group1__filter3: { included: true } + } ), + 'Selecting a conflicting filter inside a group, sets both sides to conflicted and selected.' + ); + + // Reset + model = new mw.rcfilters.dm.FiltersViewModel(); + model.initializeFilters( filterDefinition, viewsDefinition ); + + // Select a filter that has a conflict with a specific filter + model.toggleFiltersSelected( { + group1__filter2: true // conflicts: filter6 + } ); + model.reassessFilterInteractions( model.getItemByName( 'group1__filter2' ) ); + + assert.deepEqual( + model.getFullState(), + $.extend( true, {}, baseFullFilterState, { + group1__filter2: { selected: true }, + group2__filter6: { conflicted: true }, + // Subsets are affected by the selection + group1__filter3: { included: true } + } ), + 'Selecting a filter that conflicts with another filter sets the other as "conflicted".' + ); + + // Select the conflicting filter + model.toggleFiltersSelected( { + group2__filter6: true // conflicts: filter2 + } ); + + model.reassessFilterInteractions( model.getItemByName( 'group2__filter6' ) ); + + assert.deepEqual( + model.getFullState(), + $.extend( true, {}, baseFullFilterState, { + group1__filter2: { selected: true, conflicted: true }, + group2__filter6: { selected: true, conflicted: true }, + // This is added to the conflicts because filter6 is part of group2, + // who is in conflict with filter1; note that filter2 also conflicts + // with filter6 which means that filter1 conflicts with filter6 (because it's in group2) + // and also because its **own sibling** (filter2) is **also** in conflict with the + // selected items in group2 (filter6) + group1__filter1: { conflicted: true }, + + // Subsets are affected by the selection + group1__filter3: { included: true } + } ), + 'Selecting a conflicting filter with an individual filter, sets both sides to conflicted and selected.' + ); + + // Now choose a non-conflicting filter from the group + model.toggleFiltersSelected( { + group2__filter5: true + } ); + + model.reassessFilterInteractions( model.getItemByName( 'group2__filter5' ) ); + + assert.deepEqual( + model.getFullState(), + $.extend( true, {}, baseFullFilterState, { + group1__filter2: { selected: true }, + group2__filter6: { selected: true }, + group2__filter5: { selected: true }, + // Filter6 and filter1 are no longer in conflict because + // filter5, while it is in conflict with filter1, it is + // not in conflict with filter2 - and since filter2 is + // selected, it removes the conflict bidirectionally + + // Subsets are affected by the selection + group1__filter3: { included: true } + } ), + 'Selecting a non-conflicting filter within the group of a conflicting filter removes the conflicts.' + ); + + // Followup on the previous test, unselect filter2 so filter1 + // is now the only one selected in its own group, and since + // it is in conflict with the entire of group2, it means + // filter1 is once again conflicted + model.toggleFiltersSelected( { + group1__filter2: false + } ); + + model.reassessFilterInteractions( model.getItemByName( 'group1__filter2' ) ); + + assert.deepEqual( + model.getFullState(), + $.extend( true, {}, baseFullFilterState, { + group1__filter1: { conflicted: true }, + group2__filter6: { selected: true }, + group2__filter5: { selected: true } + } ), + 'Unselecting an item that did not conflict returns the conflict state.' + ); + + // Followup #2: Now actually select filter1, and make everything conflicted + model.toggleFiltersSelected( { + group1__filter1: true + } ); + + model.reassessFilterInteractions( model.getItemByName( 'group1__filter1' ) ); + + assert.deepEqual( + model.getFullState(), + $.extend( true, {}, baseFullFilterState, { + group1__filter1: { selected: true, conflicted: true }, + group2__filter6: { selected: true, conflicted: true }, + group2__filter5: { selected: true, conflicted: true }, + group2__filter4: { conflicted: true }, // Not selected but conflicted because it's in group2 + // Subsets are affected by the selection + group1__filter2: { included: true }, + group1__filter3: { included: true } + } ), + 'Selecting an item that conflicts with a whole group makes all selections in that group conflicted.' + ); + + /* Simple case */ + // Reset + model = new mw.rcfilters.dm.FiltersViewModel(); + model.initializeFilters( filterDefinition, viewsDefinition ); + + // Select a filter that has a conflict with a specific filter + model.toggleFiltersSelected( { + group1__filter2: true // conflicts: filter6 + } ); + + model.reassessFilterInteractions( model.getItemByName( 'group1__filter2' ) ); + + assert.deepEqual( + model.getFullState(), + $.extend( true, {}, baseFullFilterState, { + group1__filter2: { selected: true }, + group2__filter6: { conflicted: true }, + // Subsets are affected by the selection + group1__filter3: { included: true } + } ), + 'Simple case: Selecting a filter that conflicts with another filter sets the other as "conflicted".' + ); + + model.toggleFiltersSelected( { + group1__filter3: true // conflicts: filter6 + } ); + + model.reassessFilterInteractions( model.getItemByName( 'group1__filter3' ) ); + + assert.deepEqual( + model.getFullState(), + $.extend( true, {}, baseFullFilterState, { + group1__filter2: { selected: true }, + // Subsets are affected by the selection + group1__filter3: { selected: true, included: true } + } ), + 'Simple case: Selecting a filter that is not in conflict removes the conflict.' + ); + } ); + + QUnit.test( 'Filter highlights', function ( assert ) { + // We are using a different (smaller) definition here than the global one + var definition = [ { + name: 'group1', + title: 'Group 1', + type: 'string_options', + filters: [ + { name: 'filter1', cssClass: 'class1', label: '1', description: '1' }, + { name: 'filter2', cssClass: 'class2', label: '2', description: '2' }, + { name: 'filter3', cssClass: 'class3', label: '3', description: '3' }, + { name: 'filter4', cssClass: 'class4', label: '4', description: '4' }, + { name: 'filter5', cssClass: 'class5', label: '5', description: '5' }, + { name: 'filter6', label: '6', description: '6' } + ] + } ], + model = new mw.rcfilters.dm.FiltersViewModel(); + + model.initializeFilters( definition ); + + assert.ok( + !model.isHighlightEnabled(), + 'Initially, highlight is disabled.' + ); + + model.toggleHighlight( true ); + assert.ok( + model.isHighlightEnabled(), + 'Highlight is enabled on toggle.' + ); + + model.setHighlightColor( 'group1__filter1', 'color1' ); + model.setHighlightColor( 'group1__filter2', 'color2' ); + + assert.deepEqual( + model.getHighlightedItems().map( function ( item ) { + return item.getName(); + } ), + [ + 'group1__filter1', + 'group1__filter2' + ], + 'Highlighted items are highlighted.' + ); + + assert.equal( + model.getItemByName( 'group1__filter1' ).getHighlightColor(), + 'color1', + 'Item highlight color is set.' + ); + + model.setHighlightColor( 'group1__filter1', 'color1changed' ); + assert.equal( + model.getItemByName( 'group1__filter1' ).getHighlightColor(), + 'color1changed', + 'Item highlight color is changed on setHighlightColor.' + ); + + model.clearHighlightColor( 'group1__filter1' ); + assert.deepEqual( + model.getHighlightedItems().map( function ( item ) { + return item.getName(); + } ), + [ + 'group1__filter2' + ], + 'Clear highlight from an item results in the item no longer being highlighted.' + ); + + // Reset + model = new mw.rcfilters.dm.FiltersViewModel(); + model.initializeFilters( definition ); + + model.setHighlightColor( 'group1__filter1', 'color1' ); + model.setHighlightColor( 'group1__filter2', 'color2' ); + model.setHighlightColor( 'group1__filter3', 'color3' ); + + assert.deepEqual( + model.getHighlightedItems().map( function ( item ) { + return item.getName(); + } ), + [ + 'group1__filter1', + 'group1__filter2', + 'group1__filter3' + ], + 'Even if highlights are not enabled, the items remember their highlight state' + // NOTE: When actually displaying the highlights, the UI checks whether + // highlighting is generally active and then goes over the highlighted + // items. The item models, however, and the view model in general, still + // retains the knowledge about which filters have different colors, so we + // can seamlessly return to the colors the user previously chose if they + // reapply highlights. + ); + + // Reset + model = new mw.rcfilters.dm.FiltersViewModel(); + model.initializeFilters( definition ); + + model.setHighlightColor( 'group1__filter1', 'color1' ); + model.setHighlightColor( 'group1__filter6', 'color6' ); + + assert.deepEqual( + model.getHighlightedItems().map( function ( item ) { + return item.getName(); + } ), + [ + 'group1__filter1' + ], + 'Items without a specified class identifier are not highlighted.' + ); + } ); + + QUnit.test( 'emptyAllFilters', function ( assert ) { + var model = new mw.rcfilters.dm.FiltersViewModel(); + + model.initializeFilters( shortFilterDefinition, null ); + + model.toggleFiltersSelected( { + group1__filter1: true, + group2__filter4: true, // hidden + group3__filter5: true // sticky + } ); + + model.emptyAllFilters(); + + assert.deepEqual( + model.getSelectedState( true ), + { + group3__filter5: true, + group3__filter6: true + }, + 'Emptying filters does not affect sticky filters' + ); + } ); + + QUnit.test( 'areVisibleFiltersEmpty', function ( assert ) { + var model = new mw.rcfilters.dm.FiltersViewModel(); + model.initializeFilters( shortFilterDefinition, null ); + + model.emptyAllFilters(); + assert.ok( model.areVisibleFiltersEmpty() ); + + model.toggleFiltersSelected( { + group3__filter5: true // sticky + } ); + assert.ok( model.areVisibleFiltersEmpty() ); + + model.toggleFiltersSelected( { + group1__filter1: true + } ); + assert.notOk( model.areVisibleFiltersEmpty() ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueriesModel.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueriesModel.test.js new file mode 100644 index 00000000..ed054bd7 --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueriesModel.test.js @@ -0,0 +1,520 @@ +/* eslint-disable camelcase */ +( function ( mw ) { + var filterDefinition = [ { + name: 'group1', + type: 'send_unselected_if_any', + filters: [ + // Note: The fact filter2 is default means that in the + // filter representation, filter1 and filter3 are 'true' + { name: 'filter1', cssClass: 'filter1class' }, + { name: 'filter2', cssClass: 'filter2class', default: true }, + { name: 'filter3', cssClass: 'filter3class' } + ] + }, { + name: 'group2', + type: 'string_options', + separator: ',', + filters: [ + { name: 'filter4', cssClass: 'filter4class' }, + { name: 'filter5' }, // NOTE: Not supporting highlights! + { name: 'filter6', cssClass: 'filter6class' } + ] + }, { + name: 'group3', + type: 'boolean', + sticky: true, + filters: [ + { name: 'group3option1', cssClass: 'filter1class' }, + { name: 'group3option2', cssClass: 'filter1class' }, + { name: 'group3option3', cssClass: 'filter1class' } + ] + }, { + // Copy of the way the controller defines invert + // to check whether the conversion works + name: 'invertGroup', + type: 'boolean', + hidden: true, + filters: [ { + name: 'invert', + default: '0' + } ] + } ], + queriesFilterRepresentation = { + queries: { + 1234: { + label: 'Item converted', + data: { + filters: { + // - This value is true, but the original filter-representation + // of the saved queries ran against defaults. Since filter1 was + // set as default in the definition, the value would actually + // not appear in the representation itself. + // It is considered 'true', though, and should appear in the + // converted result in its parameter representation. + // >> group1__filter1: true, + // - The reverse is true for filter3. Filter3 is set as default + // but we don't want it in this representation of the saved query. + // Since the filter representation ran against default values, + // it will appear as 'false' value in this representation explicitly + // and the resulting parameter representation should have that + // as the result as well + group1__filter3: false, + group2__filter4: true, + group3__group3option1: true + }, + highlights: { + highlight: true, + group1__filter1: 'c5', + group3__group3option1: 'c1' + }, + invert: true + } + } + } + }, + queriesParamRepresentation = { + version: '2', + queries: { + 1234: { + label: 'Item converted', + data: { + params: { + // filter1 is 'true' so filter2 and filter3 are both '1' + // in param representation + filter2: '1', filter3: '1', + // Group type string_options + group2: 'filter4' + // Note - Group3 is sticky, so it won't show in output + }, + highlights: { + group1__filter1_color: 'c5', + group3__group3option1_color: 'c1' + } + } + } + } + }, + removeHighlights = function ( data ) { + var copy = $.extend( true, {}, data ); + copy.queries[ 1234 ].data.highlights = {}; + return copy; + }; + + QUnit.module( 'mediawiki.rcfilters - SavedQueriesModel' ); + + QUnit.test( 'Initializing queries', function ( assert ) { + var filtersModel = new mw.rcfilters.dm.FiltersViewModel(), + queriesModel = new mw.rcfilters.dm.SavedQueriesModel( filtersModel ), + exampleQueryStructure = { + version: '2', + default: '1234', + queries: { + 1234: { + label: 'Query 1234', + data: { + params: { + filter2: '1' + }, + highlights: { + group1__filter3_color: 'c2' + } + } + } + } + }, + cases = [ + { + input: {}, + finalState: { version: '2', queries: {} }, + msg: 'Empty initial query structure results in base saved queries structure.' + }, + { + input: $.extend( true, {}, exampleQueryStructure ), + finalState: $.extend( true, {}, exampleQueryStructure ), + msg: 'Initialization of given query structure does not corrupt the structure.' + }, + { + // Converting from old structure + input: $.extend( true, {}, queriesFilterRepresentation ), + finalState: $.extend( true, {}, queriesParamRepresentation ), + msg: 'Conversion from filter representation to parameters retains data.' + }, + { + // Converting from old structure + input: $.extend( true, {}, queriesFilterRepresentation, { queries: { 1234: { data: { + filters: { + // Entire group true: normalize params + filter1: true, + filter2: true, + filter3: true + }, + highlights: { + filter3: null // Get rid of empty highlight + } + } } } } ), + finalState: $.extend( true, {}, queriesParamRepresentation ), + msg: 'Conversion from filter representation to parameters normalizes params and highlights.' + }, + { + // Converting from old structure with default + input: $.extend( true, { default: '1234' }, queriesFilterRepresentation ), + finalState: $.extend( true, { default: '1234' }, queriesParamRepresentation ), + msg: 'Conversion from filter representation to parameters, with default set up, retains data.' + }, + { + // Converting from old structure and cleaning up highlights + input: $.extend( true, queriesFilterRepresentation, { queries: { 1234: { data: { highlights: { highlight: false } } } } } ), + finalState: removeHighlights( queriesParamRepresentation ), + msg: 'Conversion from filter representation to parameters and highlight cleanup' + }, + { + // New structure + input: $.extend( true, {}, queriesParamRepresentation ), + finalState: $.extend( true, {}, queriesParamRepresentation ), + msg: 'Parameter representation retains its queries structure' + }, + { + // Do not touch invalid color parameters from the initialization routine + // (Normalization, or "fixing" the query should only happen when we add new query or actively convert queries) + input: $.extend( true, { queries: { 1234: { data: { highlights: { group2__filter5_color: 'c2' } } } } }, exampleQueryStructure ), + finalState: $.extend( true, { queries: { 1234: { data: { highlights: { group2__filter5_color: 'c2' } } } } }, exampleQueryStructure ), + msg: 'Structure that contains invalid highlights remains the same in initialization' + }, + { + // Trim colors when highlight=false is stored + input: $.extend( true, { queries: { 1234: { data: { params: { highlight: '0' } } } } }, queriesParamRepresentation ), + finalState: removeHighlights( queriesParamRepresentation ), + msg: 'Colors are removed when highlight=false' + }, + { + // Remove highlight when it is true but no colors are specified + input: $.extend( true, { queries: { 1234: { data: { params: { highlight: '1' } } } } }, removeHighlights( queriesParamRepresentation ) ), + finalState: removeHighlights( queriesParamRepresentation ), + msg: 'remove highlight when it is true but there is no colors' + } + ]; + + filtersModel.initializeFilters( filterDefinition ); + + cases.forEach( function ( testCase ) { + queriesModel.initialize( testCase.input ); + assert.deepEqual( + queriesModel.getState(), + testCase.finalState, + testCase.msg + ); + } ); + } ); + + QUnit.test( 'Adding new queries', function ( assert ) { + var filtersModel = new mw.rcfilters.dm.FiltersViewModel(), + queriesModel = new mw.rcfilters.dm.SavedQueriesModel( filtersModel ), + cases = [ + { + methodParams: [ + 'label1', // Label + { // Data + filter1: '1', + filter2: '2', + group1__filter1_color: 'c2', + group1__filter3_color: 'c5' + }, + true, // isDefault + '1234' // ID + ], + result: { + itemState: { + label: 'label1', + data: { + params: { + filter1: '1', + filter2: '2' + }, + highlights: { + group1__filter1_color: 'c2', + group1__filter3_color: 'c5' + } + } + }, + isDefault: true, + id: '1234' + }, + msg: 'Given valid data is preserved.' + }, + { + methodParams: [ + 'label2', + { + filter1: '1', + invert: '1', + filter15: '1', // Invalid filter - removed + filter2: '0', // Falsey value - removed + group1__filter1_color: 'c3', + foobar: 'w00t' // Unrecognized parameter - removed + } + ], + result: { + itemState: { + label: 'label2', + data: { + params: { + filter1: '1' // Invert will be dropped because there are no namespaces + }, + highlights: { + group1__filter1_color: 'c3' + } + } + }, + isDefault: false + }, + msg: 'Given data with invalid filters and highlights is normalized' + } + ]; + + filtersModel.initializeFilters( filterDefinition ); + + // Start with an empty saved queries model + queriesModel.initialize( {} ); + + cases.forEach( function ( testCase ) { + var itemID = queriesModel.addNewQuery.apply( queriesModel, testCase.methodParams ), + item = queriesModel.getItemByID( itemID ); + + assert.deepEqual( + item.getState(), + testCase.result.itemState, + testCase.msg + ' (itemState)' + ); + + assert.equal( + item.isDefault(), + testCase.result.isDefault, + testCase.msg + ' (isDefault)' + ); + + if ( testCase.result.id !== undefined ) { + assert.equal( + item.getID(), + testCase.result.id, + testCase.msg + ' (item ID)' + ); + } + } ); + } ); + + QUnit.test( 'Manipulating queries', function ( assert ) { + var id1, id2, item1, matchingItem, + queriesStructure = {}, + filtersModel = new mw.rcfilters.dm.FiltersViewModel(), + queriesModel = new mw.rcfilters.dm.SavedQueriesModel( filtersModel ); + + filtersModel.initializeFilters( filterDefinition ); + + // Start with an empty saved queries model + queriesModel.initialize( {} ); + + // Add items + id1 = queriesModel.addNewQuery( + 'New query 1', + { + group2: 'filter5', + group1__filter1_color: 'c5', + group3__group3option1_color: 'c1' + } + ); + id2 = queriesModel.addNewQuery( + 'New query 2', + { + filter1: '1', + filter2: '1', + invert: '1' + } + ); + item1 = queriesModel.getItemByID( id1 ); + + assert.equal( + item1.getID(), + id1, + 'Item created and its data retained successfully' + ); + + // NOTE: All other methods that the item itself returns are + // tested in the dm.SavedQueryItemModel.test.js file + + // Build the query structure we expect per item + queriesStructure[ id1 ] = { + label: 'New query 1', + data: { + params: { + group2: 'filter5' + }, + highlights: { + group1__filter1_color: 'c5', + group3__group3option1_color: 'c1' + } + } + }; + queriesStructure[ id2 ] = { + label: 'New query 2', + data: { + params: { + filter1: '1', + filter2: '1' + }, + highlights: {} + } + }; + + assert.deepEqual( + queriesModel.getState(), + { + version: '2', + queries: queriesStructure + }, + 'Full query represents current state of items' + ); + + // Add default + queriesModel.setDefault( id2 ); + + assert.deepEqual( + queriesModel.getState(), + { + version: '2', + default: id2, + queries: queriesStructure + }, + 'Setting default is reflected in queries state' + ); + + // Remove default + queriesModel.setDefault( null ); + + assert.deepEqual( + queriesModel.getState(), + { + version: '2', + queries: queriesStructure + }, + 'Removing default is reflected in queries state' + ); + + // Find matching query + matchingItem = queriesModel.findMatchingQuery( + { + group2: 'filter5', + group1__filter1_color: 'c5', + group3__group3option1_color: 'c1' + } + ); + assert.deepEqual( + matchingItem.getID(), + id1, + 'Finding matching item by identical state' + ); + + // Find matching query with 0-values (base state) + matchingItem = queriesModel.findMatchingQuery( + { + group2: 'filter5', + filter1: '0', + filter2: '0', + group1__filter1_color: 'c5', + group3__group3option1_color: 'c1' + } + ); + assert.deepEqual( + matchingItem.getID(), + id1, + 'Finding matching item by "dirty" state with 0-base values' + ); + } ); + + QUnit.test( 'Testing invert property', function ( assert ) { + var itemID, item, + filtersModel = new mw.rcfilters.dm.FiltersViewModel(), + queriesModel = new mw.rcfilters.dm.SavedQueriesModel( filtersModel ), + viewsDefinition = { + namespace: { + label: 'Namespaces', + trigger: ':', + groups: [ { + name: 'namespace', + label: 'Namespaces', + type: 'string_options', + separator: ';', + filters: [ + { name: 0, label: 'Main', cssClass: 'namespace-0' }, + { name: 1, label: 'Talk', cssClass: 'namespace-1' }, + { name: 2, label: 'User', cssClass: 'namespace-2' }, + { name: 3, label: 'User talk', cssClass: 'namespace-3' } + ] + } ] + } + }; + + filtersModel.initializeFilters( filterDefinition, viewsDefinition ); + + // Start with an empty saved queries model + queriesModel.initialize( {} ); + + filtersModel.toggleFiltersSelected( { + group1__filter3: true, + invertGroup__invert: true + } ); + itemID = queriesModel.addNewQuery( + 'label1', // Label + filtersModel.getMinimizedParamRepresentation(), + true, // isDefault + '2345' // ID + ); + item = queriesModel.getItemByID( itemID ); + + assert.deepEqual( + item.getState(), + { + label: 'label1', + data: { + params: { + filter1: '1', + filter2: '1' + }, + highlights: {} + } + }, + 'Invert parameter is not saved if there are no namespaces.' + ); + + // Reset + filtersModel.initializeFilters( filterDefinition, viewsDefinition ); + filtersModel.toggleFiltersSelected( { + group1__filter3: true, + invertGroup__invert: true, + namespace__1: true + } ); + itemID = queriesModel.addNewQuery( + 'label1', // Label + filtersModel.getMinimizedParamRepresentation(), + true, // isDefault + '1234' // ID + ); + item = queriesModel.getItemByID( itemID ); + + assert.deepEqual( + item.getState(), + { + label: 'label1', + data: { + params: { + filter1: '1', + filter2: '1', + invert: '1', + namespace: '1' + }, + highlights: {} + } + }, + 'Invert parameter saved if there are namespaces.' + ); + } ); +}( mediaWiki ) ); diff --git a/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueryItemModel.test.js b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueryItemModel.test.js new file mode 100644 index 00000000..181e9925 --- /dev/null +++ b/www/wiki/tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueryItemModel.test.js @@ -0,0 +1,89 @@ +/* eslint-disable camelcase */ +( function ( mw ) { + var itemData = { + params: { + param1: '1', + param2: 'foo|bar', + invert: '0' + }, + highlights: { + param1_color: 'c1', + param2_color: 'c2' + } + }; + + QUnit.module( 'mediawiki.rcfilters - SavedQueryItemModel' ); + + QUnit.test( 'Initializing and getters', function ( assert ) { + var model; + + model = new mw.rcfilters.dm.SavedQueryItemModel( + 'randomID', + 'Some label', + $.extend( true, {}, itemData ) + ); + + assert.equal( + model.getID(), + 'randomID', + 'Item ID is retained' + ); + + assert.equal( + model.getLabel(), + 'Some label', + 'Item label is retained' + ); + + assert.deepEqual( + model.getData(), + itemData, + 'Item data is retained' + ); + + assert.ok( + !model.isDefault(), + 'Item default state is retained.' + ); + } ); + + QUnit.test( 'Default', function ( assert ) { + var model; + + model = new mw.rcfilters.dm.SavedQueryItemModel( + 'randomID', + 'Some label', + $.extend( true, {}, itemData ) + ); + + assert.ok( + !model.isDefault(), + 'Default state represented when item initialized with default:false.' + ); + + model.toggleDefault( true ); + assert.ok( + model.isDefault(), + 'Default state toggles to true successfully' + ); + + model.toggleDefault( false ); + assert.ok( + !model.isDefault(), + 'Default state toggles to false successfully' + ); + + // Reset + model = new mw.rcfilters.dm.SavedQueryItemModel( + 'randomID', + 'Some label', + $.extend( true, {}, itemData ), + { default: true } + ); + + assert.ok( + model.isDefault(), + 'Default state represented when item initialized with default:true.' + ); + } ); +}( mediaWiki ) ); |