Mooeypoo has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/357124 )

Change subject: RCFilters: Add UriProcessor and unit tests for URL manipulations
......................................................................

RCFilters: Add UriProcessor and unit tests for URL manipulations

Bug: T166974
Change-Id: I066c33a01770b7d8026aa3e039af5fe5d9c4cdf9
---
M resources/Resources.php
M resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
M resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
A resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
M tests/qunit/QUnitTestResources.php
A tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js
6 files changed, 646 insertions(+), 187 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core 
refs/changes/24/357124/1

diff --git a/resources/Resources.php b/resources/Resources.php
index c4baab7..dd62a84 100644
--- a/resources/Resources.php
+++ b/resources/Resources.php
@@ -1749,6 +1749,7 @@
                        
'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js',
                        
'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ChangesListViewModel.js',
                        
'resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js',
+                       
'resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js',
                ],
                'dependencies' => [
                        'oojs',
diff --git 
a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js 
b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
index 59c0a19..dd698cd 100644
--- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
+++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
@@ -75,8 +75,8 @@
                        var subsetNames = [],
                                filterItem = new mw.rcfilters.dm.FilterItem( 
filter.name, model, {
                                        group: model.getName(),
-                                       label: mw.msg( filter.label ),
-                                       description: mw.msg( filter.description 
),
+                                       label: filter.label ? mw.msg( 
filter.label ) : filter.name,
+                                       description: filter.description ? 
mw.msg( filter.description ) : '',
                                        cssClass: filter.cssClass
                                } );
 
diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js 
b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
index 897162a..09c889e 100644
--- a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
+++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
@@ -13,7 +13,7 @@
                this.savedQueriesModel = savedQueriesModel;
                this.requestCounter = 0;
                this.baseFilterState = {};
-               this.emptyParameterState = {};
+               this.uriProcessor = null;
                this.initializing = false;
        };
 
@@ -26,7 +26,7 @@
         * @param {Array} filterStructure Filter definition and structure for 
the model
         */
        mw.rcfilters.Controller.prototype.initialize = function ( 
filterStructure ) {
-               var parsedSavedQueries, validParameterNames, baseParams,
+               var parsedSavedQueries,
                        useDataFromServer = false,
                        uri = new mw.Uri(),
                        $changesList = $( '.mw-changeslist' 
).first().contents();
@@ -35,14 +35,9 @@
                this.filtersModel.initializeFilters( filterStructure );
 
                this._buildBaseFilterState();
-               this._buildEmptyParameterState();
-               validParameterNames = Object.keys( 
this._getEmptyParameterState() )
-                       .filter( function ( param ) {
-                               // Remove 'highlight' parameter from this check;
-                               // if it's the only parameter in the URL we 
still
-                               // want to consider the URL 'empty' for 
defaults to load
-                               return param !== 'highlight';
-                       } );
+               this.uriProcessor = new mw.rcfilters.UriProcessor(
+                       this.filtersModel
+               );
 
                try {
                        parsedSavedQueries = JSON.parse( mw.user.options.get( 
'rcfilters-saved-queries' ) || '{}' );
@@ -67,11 +62,7 @@
                // Defaults should only be applied on load (if necessary)
                // or on request
                this.initializing = true;
-               if (
-                       Object.keys( uri.query ).some( function ( parameter ) {
-                               return validParameterNames.indexOf( parameter ) 
> -1;
-                       } )
-               ) {
+               if ( this.uriProcessor.isQueryValidForLoad( uri.query ) ) {
                        // There are parameters in the url, update model state
                        this.updateStateBasedOnUrl();
                        useDataFromServer = true;
@@ -85,25 +76,7 @@
                                this.applySavedQuery( 
this.savedQueriesModel.getDefault() );
                                useDataFromServer = false;
                        } else {
-                               baseParams = 
this.filtersModel.getDefaultParams();
-                               if ( uri.query.urlversion === '2' ) {
-                                       baseParams = {};
-                               }
-
-                               this._updateModelState(
-                                       $.extend(
-                                               true,
-                                               baseParams,
-                                               // We've ignored the highlight 
and invert parameters for
-                                               // the sake of seeing whether 
we need to apply defaults - but
-                                               // while we do load the 
defaults, we still want to retain
-                                               // the actual value given in 
the URL for it on top of the
-                                               // defaults
-                                               {
-                                                       highlight: String( 
Number( uri.query.highlight ) )
-                                               }
-                                       )
-                               );
+                               this.uriProcessor.updateModelBasedOnQuery( 
uri.query );
                                // In this case, there's no need to re-request 
the AJAX call
                                // the initial data will be processed because 
we are getting
                                // exactly what the server produced for us
@@ -126,7 +99,7 @@
         * Reset to default filters
         */
        mw.rcfilters.Controller.prototype.resetToDefaults = function () {
-               this._updateModelState( $.extend( true, { highlight: '0' }, 
this._getDefaultParams() ) );
+               this.uriProcessor.updateModelBasedOnQuery( 
this._getDefaultParams() );
                this.updateChangesList();
        };
 
@@ -409,24 +382,6 @@
        };
 
        /**
-        * Build an empty representation of the parameters, where all parameters
-        * are either set to '0' or '' depending on their type.
-        * This must run during initialization, before highlights are set.
-        */
-       mw.rcfilters.Controller.prototype._buildEmptyParameterState = function 
() {
-               var emptyParams = this.filtersModel.getParametersFromFilters( 
{} ),
-                       emptyHighlights = 
this.filtersModel.getHighlightParameters();
-
-               this.emptyParameterState = $.extend(
-                       true,
-                       {},
-                       emptyParams,
-                       emptyHighlights,
-                       { highlight: '0' }
-               );
-       };
-
-       /**
         * Get an object representing the base filter state of both
         * filters and highlights. The structure is similar to what we use
         * to store each query in the saved queries object:
@@ -444,22 +399,6 @@
         */
        mw.rcfilters.Controller.prototype._getBaseFilterState = function () {
                return this.baseFilterState;
-       };
-
-       /**
-        * Get an object representing the base state of parameters
-        * and highlights. The structure is similar to what we use
-        * to store each query in the saved queries object:
-        * {
-        *    param1: "value",
-        *    param2: "value1|value2"
-        * }
-        *
-        * @return {Object} Object representing the base state of
-        *  parameters and highlights
-        */
-       mw.rcfilters.Controller.prototype._getEmptyParameterState = function () 
{
-               return this.emptyParameterState;
        };
 
        /**
@@ -526,11 +465,7 @@
         * without adding an history entry.
         */
        mw.rcfilters.Controller.prototype.replaceUrl = function () {
-               window.history.replaceState(
-                       { tag: 'rcfilters' },
-                       document.title,
-                       this._getUpdatedUri().toString()
-               );
+               mw.rcfilters.UriProcessor.static.replaceState( 
this._getUpdatedUri() );
        };
 
        /**
@@ -538,22 +473,7 @@
         * on current URL values.
         */
        mw.rcfilters.Controller.prototype.updateStateBasedOnUrl = function () {
-               var uri = new mw.Uri(),
-                       base = this.filtersModel.getDefaultParams();
-
-               // Check whether we are dealing with urlversion=2
-               // If we are, we do not merge the initial request with
-               // defaults. Not having urlversion=2 means we need to
-               // reproduce the server-side request and merge the
-               // requested parameters (or starting state) with the
-               // wiki default.
-               // Any subsequent change of the URL through the RCFilters
-               // system will receive 'urlversion=2'
-               if ( uri.query.urlversion === '2' ) {
-                       base = {};
-               }
-
-               this._updateModelState( $.extend( true, {}, base, uri.query ) );
+               this.uriProcessor.updateModelBasedOnQuery( new mw.Uri().query );
                this.updateChangesList();
        };
 
@@ -578,35 +498,40 @@
        };
 
        /**
-        * Update the model state from given the given parameters.
+        * Get an object representing the default parameter state, whether
+        * it is from the model defaults or from the saved queries.
         *
-        * This is an internal method, and should only be used from inside
-        * the controller.
-        *
-        * @param {Object} parameters Object representing the parameters for
-        *  filters and highlights
+        * @return {Object} Default parameters
         */
-       mw.rcfilters.Controller.prototype._updateModelState = function ( 
parameters ) {
-               // Update filter states
-               this.filtersModel.toggleFiltersSelected(
-                       this.filtersModel.getFiltersFromParameters(
-                               parameters
-                       )
+       mw.rcfilters.Controller.prototype._getDefaultParams = function () {
+               var data, queryHighlights,
+                       savedParams = {},
+                       savedHighlights = {},
+                       defaultSavedQueryItem = 
this.savedQueriesModel.getItemByID( this.savedQueriesModel.getDefault() );
+
+               if ( mw.config.get( 'wgStructuredChangeFiltersEnableSaving' ) &&
+                       defaultSavedQueryItem ) {
+
+                       data = defaultSavedQueryItem.getData();
+
+                       queryHighlights = data.highlights || {};
+                       savedParams = 
this.filtersModel.getParametersFromFilters( data.filters || {} );
+
+                       // Translate highlights to parameters
+                       savedHighlights.highlight = String( Number( 
queryHighlights.highlight ) );
+                       $.each( queryHighlights, function ( filterName, color ) 
{
+                               if ( filterName !== 'highlights' ) {
+                                       savedHighlights[ filterName + '_color' 
] = color;
+                               }
+                       } );
+
+                       return $.extend( true, {}, savedParams, savedHighlights 
);
+               }
+
+               return $.extend(
+                       { highlight: '0' },
+                       this.filtersModel.getDefaultParams()
                );
-
-               // Update highlight state
-               this.filtersModel.toggleHighlight( !!Number( 
parameters.highlight ) );
-               this.filtersModel.getItems().forEach( function ( filterItem ) {
-                       var color = parameters[ filterItem.getName() + '_color' 
];
-                       if ( color ) {
-                               filterItem.setHighlightColor( color );
-                       } else {
-                               filterItem.clearHighlightColor();
-                       }
-               } );
-
-               // Check all filter interactions
-               this.filtersModel.reassessFilterInteractions();
        };
 
        /**
@@ -620,48 +545,21 @@
         * @param {Object} [params] Extra parameters to add to the API call
         */
        mw.rcfilters.Controller.prototype._updateURL = function ( params ) {
-               var currentFilterState, updatedFilterState, updatedUri,
-                       uri = new mw.Uri(),
-                       notEquivalent = function ( obj1, obj2 ) {
-                               var keys = Object.keys( obj1 ).concat( 
Object.keys( obj2 ) );
-                               return keys.some( function ( key ) {
-                                       return obj1[ key ] != obj2[ key ]; // 
eslint-disable-line eqeqeq
-                               } );
-                       };
+               var currentUri = new mw.Uri(),
+                       updatedUri = this._getUpdatedUri();
 
-               params = params || {};
-
-               updatedUri = this._getUpdatedUri();
-               updatedUri.extend( params );
-
-               // Compare states instead of parameters
-               // This will allow us to always have a proper check of whether
-               // the requested new url is one to change or not, regardless of
-               // actual parameter visibility/representation in the URL
-               currentFilterState = 
this.filtersModel.getFiltersFromParameters( uri.query );
-               updatedFilterState = 
this.filtersModel.getFiltersFromParameters( updatedUri.query );
-               // Include highlight states
-               $.extend( true,
-                       currentFilterState,
-                       this.filtersModel.extractHighlightValues( uri.query ),
-                       { highlight: !!Number( uri.query.highlight ) }
-               );
-               $.extend( true,
-                       updatedFilterState,
-                       this.filtersModel.extractHighlightValues( 
updatedUri.query ),
-                       { highlight: !!Number( updatedUri.query.highlight ) }
-               );
+               updatedUri.extend( params || {} );
 
                if (
-                       uri.query.urlversion !== '2' ||
-                       notEquivalent( currentFilterState, updatedFilterState )
+                       this.uriProcessor.getVersion( currentUri.query ) !== 2 
||
+                       this.uriProcessor.isNewState( currentUri.query, 
updatedUri.query )
                ) {
                        if ( this.initializing ) {
                                // Initially, when we just build the first page 
load
                                // out of defaults, we want to replace the 
history
-                               window.history.replaceState( { tag: 'rcfilters' 
}, document.title, updatedUri.toString() );
+                               mw.rcfilters.UriProcessor.static.replaceState( 
updatedUri );
                        } else {
-                               window.history.pushState( { tag: 'rcfilters' }, 
document.title, updatedUri.toString() );
+                               mw.rcfilters.UriProcessor.static.pushState( 
updatedUri );
                        }
                }
        };
@@ -672,41 +570,8 @@
         * @return {mw.Uri} Updated Uri
         */
        mw.rcfilters.Controller.prototype._getUpdatedUri = function () {
-               var uri = new mw.Uri(),
-                       highlightParams = 
this.filtersModel.getHighlightParameters(),
-                       modelParameters = 
this.filtersModel.getParametersFromFilters(),
-                       baseParams = this._getEmptyParameterState();
-
-               // Minimize values of the model parameters; show only the 
values that
-               // are non-zero. We assume that all parameters that are not 
literally
-               // showing in the URL are set to zero or empty
-               $.each( modelParameters, function ( paramName, value ) {
-                       if ( baseParams[ paramName ] !== value ) {
-                               uri.query[ paramName ] = value;
-                       } else {
-                               // We need to remove this value from the url
-                               delete uri.query[ paramName ];
-                       }
-               } );
-
-               // highlight params
-               $.each( highlightParams, function ( paramName, value ) {
-                       // Only output if it is different than the base 
parameters
-                       if ( baseParams[ paramName ] !== value ) {
-                               uri.query[ paramName ] = value;
-                       } else {
-                               delete uri.query[ paramName ];
-                       }
-               } );
-
-               if ( this.filtersModel.isHighlightEnabled() ) {
-                       uri.query.highlight = '1';
-               } else {
-                       delete uri.query.highlight;
-               }
-
-               // Add the urlversion=2 param for all URLs made by the 
RCFilters system
-               uri.query.urlversion = '2';
+               var uri = new mw.Uri();
+               uri.extend( this.uriProcessor.getUpdatedUriQuery( uri.query ) );
 
                return uri;
        };
diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js 
b/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
new file mode 100644
index 0000000..35e24ed
--- /dev/null
+++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
@@ -0,0 +1,286 @@
+( function ( mw, $ ) {
+       /* eslint no-underscore-dangle: "off" */
+       /**
+        * URI Processor for RCFilters
+        *
+        * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters view 
model
+        * @param {Object} [config] Configuration options
+        */
+       mw.rcfilters.UriProcessor = function MwRcfiltersController( 
filtersModel ) {
+               this.emptyParameterState = {};
+               this.filtersModel = filtersModel;
+
+               // Initialize
+               this._buildEmptyParameterState();
+       };
+
+       /* Initialization */
+       OO.initClass( mw.rcfilters.UriProcessor );
+
+       /* Static methods */
+
+       /**
+        * Replace the url history through replaceState
+        *
+        * @param {mw.Uri} newUri New URI to replace
+        */
+       mw.rcfilters.UriProcessor.static.replaceState = function ( newUri ) {
+               window.history.replaceState(
+                       { tag: 'rcfilters' },
+                       document.title,
+                       newUri.toString()
+               );
+       };
+
+       /**
+        * Push the url to history through pushState
+        *
+        * @param {mw.Uri} newUri New URI to push
+        */
+       mw.rcfilters.UriProcessor.static.pushState = function ( newUri ) {
+               window.history.pushState(
+                       { tag: 'rcfilters' },
+                       document.title,
+                       newUri.toString()
+               );
+       };
+
+       /* Methods */
+
+       /**
+        * Get the version that this URL query is tagged with.
+        *
+        * @param {Object} [uriQuery] URI query
+        * @return {Number} URL version
+        */
+       mw.rcfilters.UriProcessor.prototype.getVersion = function ( uriQuery ) {
+               uriQuery = uriQuery || new mw.Uri().query;
+
+               return Number( uriQuery.urlversion || 1 );
+       };
+
+       /**
+        * Update the filters model based on the URI query
+        *
+        * @param {Object} uriQuery URI query
+        */
+       mw.rcfilters.UriProcessor.prototype.updateModelBasedOnQuery = function 
( uriQuery ) {
+               var parameters = this.getNormalizedQueryParams( uriQuery || new 
mw.Uri().query );
+
+               // Update filter states
+               this.filtersModel.toggleFiltersSelected(
+                       this.filtersModel.getFiltersFromParameters(
+                               parameters
+                       )
+               );
+
+               // Update highlight state
+               this.filtersModel.toggleHighlight( !!Number( 
parameters.highlight ) );
+               this.filtersModel.getItems().forEach( function ( filterItem ) {
+                       var color = parameters[ filterItem.getName() + '_color' 
];
+                       if ( color ) {
+                               filterItem.setHighlightColor( color );
+                       } else {
+                               filterItem.clearHighlightColor();
+                       }
+               } );
+
+               // Check all filter interactions
+               this.filtersModel.reassessFilterInteractions();
+       };
+
+       /**
+        * Get an updated mw.Uri object based on the model state
+        *
+        * @param {Object} [currentUriQuery] Current Uri object to manipulate
+        * @return {Object} Updated Uri query
+        */
+       mw.rcfilters.UriProcessor.prototype.getUpdatedUriQuery = function ( 
currentUriQuery ) {
+               var paramsFromModel = this.getUriParametersFromModel();
+
+               currentUriQuery = currentUriQuery || new mw.Uri().query;
+
+               // See if we need to merge defaults
+               if ( this.getVersion( currentUriQuery ) !== 2 ) {
+                       // Merge with defaults if we don't have url version 2
+                       $.extend(
+                               true,
+                               currentUriQuery,
+                               this.filtersModel.getDefaultParams()
+                       );
+               }
+
+               // Add the urlversion=2 param for all URLs made by the 
RCFilters system
+               currentUriQuery.urlversion = '2';
+
+               // We want to still redo the minimization when we merge model 
params with the
+               // initial query, because there may have been a case
+               // where model parameters overrode the original (default) 
parameters with a
+               // state that is now part of the base state.
+               return this._minimizeQuery(
+                       $.extend(
+                               true,
+                               {},
+                               currentUriQuery,
+                               paramsFromModel
+                       )
+               );
+       };
+
+       /**
+        * Get parameters representing the current state of the model
+        *
+        * @return {Object} Uri query parameters
+        */
+       mw.rcfilters.UriProcessor.prototype.getUriParametersFromModel = 
function () {
+               return this._minimizeQuery( $.extend(
+                       true,
+                       {},
+                       this.filtersModel.getParametersFromFilters(),
+                       this.filtersModel.getHighlightParameters(),
+                       { highlight: String( Number( 
this.filtersModel.isHighlightEnabled() ) ) }
+               ) );
+       };
+
+       /**
+        * Remove all parameters that have the same value as the base state
+        *
+        * @private
+        * @param {Object} uriQuery Current uri query
+        * @return {Object} Minimized query
+        */
+       mw.rcfilters.UriProcessor.prototype._minimizeQuery = function ( 
uriQuery ) {
+               var baseParams = this.getEmptyParameterState();
+
+               $.each( uriQuery, function ( paramName, paramValue ) {
+                       if ( baseParams[ paramName ] === paramValue ) {
+                               // Remove parameter from query
+                               delete uriQuery[ paramName ];
+                       }
+               } );
+
+               return uriQuery;
+       };
+       /**
+        * Compare two URI queries to decide whether they are different
+        * enough to represent a new state.
+        *
+        * @param {Object} currentUriQuery Current Uri query
+        * @param {Object} updatedUriQuery Updated Uri query
+        * @return {boolean} This is a new state
+        */
+       mw.rcfilters.UriProcessor.prototype.isNewState = function ( 
currentUriQuery, updatedUriQuery ) {
+               var currentFilterState, updatedFilterState,
+                       notEquivalent = function ( obj1, obj2 ) {
+                               var keys = Object.keys( obj1 ).concat( 
Object.keys( obj2 ) );
+                               return keys.some( function ( key ) {
+                                       return obj1[ key ] != obj2[ key ]; // 
eslint-disable-line eqeqeq
+                               } );
+                       };
+
+               // Compare states instead of parameters
+               // This will allow us to always have a proper check of whether
+               // the requested new url is one to change or not, regardless of
+               // actual parameter visibility/representation in the URL
+               currentFilterState = this._buildFullFilterState( 
currentUriQuery );
+               updatedFilterState = this._buildFullFilterState( 
updatedUriQuery );
+
+               return notEquivalent( currentFilterState, updatedFilterState );
+       };
+
+       /**
+        * Check whether the given query has parameters that are
+        * recognized as parameters we should load the system with
+        *
+        * @param {mw.Uri} [uriQuery] Given URI query
+        * @return {boolean} Query contains valid recognized parameters
+        */
+       mw.rcfilters.UriProcessor.prototype.isQueryValidForLoad = function ( 
uriQuery ) {
+               var anyValidInUrl,
+                       validParameterNames = Object.keys( 
this.getEmptyParameterState() )
+                       .filter( function ( param ) {
+                               // Remove 'highlight' parameter from this check;
+                               // if it's the only parameter in the URL we 
still
+                               // want to consider the URL 'empty' for 
defaults to load
+                               return param !== 'highlight';
+                       } );
+
+               uriQuery = uriQuery || new mw.Uri().query;
+
+               anyValidInUrl = Object.keys( uriQuery ).some( function ( 
parameter ) {
+                       return validParameterNames.indexOf( parameter ) > -1;
+               } );
+
+               // URL version 2 is allowed to be empty or within nonrecognized 
params
+               return anyValidInUrl || ( this.getVersion( uriQuery ) === 2 );
+       };
+
+       /**
+        * Get the adjusted URI params based on the url version
+        * If the urlversion is not 2, the parameters are merged with
+        * the model's defaults.
+        *
+        * @param {Object} uriQuery Current URI query
+        * @return {Object} Normalized parameters
+        */
+       mw.rcfilters.UriProcessor.prototype.getNormalizedQueryParams = function 
( uriQuery ) {
+               // Check whether we are dealing with urlversion=2
+               // If we are, we do not merge the initial request with
+               // defaults. Not having urlversion=2 means we need to
+               // reproduce the server-side request and merge the
+               // requested parameters (or starting state) with the
+               // wiki default.
+               // Any subsequent change of the URL through the RCFilters
+               // system will receive 'urlversion=2'
+               var base = this.getVersion( uriQuery ) === 2 ?
+                       {} :
+                       this.filtersModel.getDefaultParams();
+
+               return this._minimizeQuery( $.extend( true, base, uriQuery, { 
urlversion: '2' } ) );
+       };
+
+       /**
+        * Get the representation of an empty parameter state
+        *
+        * @return {Object} Empty parameter state
+        */
+       mw.rcfilters.UriProcessor.prototype.getEmptyParameterState = function 
() {
+               return this.emptyParameterState;
+       };
+
+       /**
+        * Build the full filter state based on parameters
+        *
+        * @private
+        * @param {Object} uriQuery Given URI query
+        * @return {Object} Full filter state representing the URI query
+        */
+       mw.rcfilters.UriProcessor.prototype._buildFullFilterState = function ( 
uriQuery ) {
+               return $.extend( true,
+                       {},
+                       this.filtersModel.getFiltersFromParameters( uriQuery ),
+                       this.filtersModel.extractHighlightValues( uriQuery ),
+                       { highlight: !!Number( uriQuery.highlight ) }
+               );
+       };
+
+       /**
+        * Build an empty representation of the parameters, where all parameters
+        * are either set to '0' or '' depending on their type.
+        * This must run during initialization, before highlights are set.
+        *
+        * @private
+        */
+       mw.rcfilters.UriProcessor.prototype._buildEmptyParameterState = 
function () {
+               var emptyParams = this.filtersModel.getParametersFromFilters( 
{} ),
+                       emptyHighlights = 
this.filtersModel.getHighlightParameters();
+
+               this.emptyParameterState = $.extend(
+                       true,
+                       {},
+                       emptyParams,
+                       emptyHighlights,
+                       { highlight: '0' }
+               );
+       };
+}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/QUnitTestResources.php 
b/tests/qunit/QUnitTestResources.php
index 53362c4..ee3cd5b 100644
--- a/tests/qunit/QUnitTestResources.php
+++ b/tests/qunit/QUnitTestResources.php
@@ -94,6 +94,7 @@
                        
'tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js',
                        
'tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js',
                        
'tests/qunit/suites/resources/mediawiki.rcfilters/dm.FilterItem.test.js',
+                       
'tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js',
                        
'tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js',
                        
'tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js',
                        
'tests/qunit/suites/resources/mediawiki/mediawiki.cookie.test.js',
diff --git 
a/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js 
b/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js
new file mode 100644
index 0000000..0b398ea
--- /dev/null
+++ b/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js
@@ -0,0 +1,306 @@
+/* eslint-disable camelcase */
+( function ( mw, $ ) {
+       var mockFilterStructure = [ {
+                       name: 'group1',
+                       title: 'Group 1',
+                       type: 'send_unselected_if_any',
+                       filters: [
+                               { name: 'filter1', default: true },
+                               { name: 'filter2' }
+                       ]
+               }, {
+                       name: 'group2',
+                       title: 'Group 2',
+                       type: 'send_unselected_if_any',
+                       filters: [
+                               { name: 'filter3' },
+                               { name: 'filter4', default: true }
+                       ]
+               }, {
+                       name: 'group3',
+                       title: 'Group 3',
+                       type: 'string_options',
+                       filters: [
+                               { name: 'filter5' },
+                               { name: 'filter6' }
+                       ]
+               } ],
+               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( 'updateModelBasedOnQuery', function ( assert ) {
+               var uriProcessor,
+                       filtersModel = new mw.rcfilters.dm.FiltersViewModel();
+
+               filtersModel.initializeFilters( mockFilterStructure );
+               uriProcessor = new mw.rcfilters.UriProcessor( filtersModel );
+
+               uriProcessor.updateModelBasedOnQuery( {} );
+               assert.deepEqual(
+                       uriProcessor.getUriParametersFromModel(),
+                       minimalDefaultParams,
+                       'Version 1: Empty url query sets model to defaults'
+               );
+
+               uriProcessor.updateModelBasedOnQuery( { urlversion: '2' } );
+               assert.deepEqual(
+                       uriProcessor.getUriParametersFromModel(),
+                       {},
+                       'Version 2: Empty url query sets model to all-false'
+               );
+
+               uriProcessor.updateModelBasedOnQuery( { filter1: '1', 
urlversion: '2' } );
+               assert.deepEqual(
+                       uriProcessor.getUriParametersFromModel(),
+                       { filter1: '1' },
+                       'Parameters in Uri query set parameter value in the 
model'
+               );
+
+               uriProcessor.updateModelBasedOnQuery( { highlight: '1', 
group1__filter1_color: 'c1', urlversion: '2' } );
+               assert.deepEqual(
+                       uriProcessor.getUriParametersFromModel(),
+                       {
+                               highlight: '1',
+                               group1__filter1_color: 'c1'
+                       },
+                       'Highlight parameters in Uri query set highlight state 
in the model'
+               );
+       } );
+
+       QUnit.test( 'getUpdatedUriQuery', function ( assert ) {
+               var uriProcessor,
+                       filtersModel = new mw.rcfilters.dm.FiltersViewModel();
+
+               filtersModel.initializeFilters( mockFilterStructure );
+               uriProcessor = new mw.rcfilters.UriProcessor( filtersModel );
+
+               assert.deepEqual(
+                       uriProcessor.getUpdatedUriQuery( {} ),
+                       $.extend( true, { urlversion: '2' }, 
minimalDefaultParams ),
+                       'Initially empty URI query without urlversion=2 results 
with defaults plus urlversion=2'
+               );
+
+               assert.deepEqual(
+                       uriProcessor.getUpdatedUriQuery( { urlversion: '2' } ),
+                       { urlversion: '2' },
+                       'Initially empty URI query with urlversion=2 does not 
merge defaults'
+               );
+
+               assert.deepEqual(
+                       uriProcessor.getUpdatedUriQuery( { filter1: '0' } ),
+                       {
+                               filter1: '1',
+                               filter4: '1',
+                               urlversion: '2'
+                       },
+                       'When getting an updated URL (with merging defaults) we 
consider only filter state and not query params.'
+               );
+
+               assert.deepEqual(
+                       uriProcessor.getUpdatedUriQuery( { filter1: '1', 
urlversion: '2' } ),
+                       {
+                               filter1: '1',
+                               urlversion: '2'
+                       },
+                       'When getting an updated URL (without merging defaults) 
we consider only filter state and not query params.'
+               );
+
+               assert.deepEqual(
+                       uriProcessor.getUpdatedUriQuery( { foo: 'bar', 
urlversion: '2' } ),
+                       {
+                               foo: 'bar',
+                               urlversion: '2'
+                       },
+                       'URI retains all unrecognized parameters'
+               );
+
+               filtersModel.toggleFiltersSelected( {
+                       // Setting filter1 as true is actually
+                       // setting filter2 **parameter** as true
+                       group1__filter1: true
+               } );
+
+               assert.deepEqual(
+                       uriProcessor.getUpdatedUriQuery( { urlversion: '2' } ),
+                       {
+                               filter2: '1',
+                               urlversion: '2'
+                       },
+                       'When initializing the URL (version 2) we output 
parameters representing filter state.'
+               );
+               assert.deepEqual(
+                       uriProcessor.getUpdatedUriQuery( {} ),
+                       {
+                               filter1: '1',
+                               filter2: '1',
+                               filter4: '1',
+                               urlversion: '2'
+                       },
+                       'When initializing the URL (not version 2) we output 
parameters representing filter state with defaults.'
+               );
+       } );
+
+       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: false,
+                                       // Note that the RCFilters system can't 
add unrecognized parameters
+                                       // so this is a test for sanity check 
and to make sure that on initialization
+                                       // we recognize correct states of the 
url and not lose unrecognized params
+                                       // (See also testsgetUpdatedUriQuery 
for preservation of unrecognized params)
+                                       message: 'Added unrecognized parameters 
are not 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( 'isQueryValidForLoad', 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.isQueryValidForLoad( 
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
+                       );
+               } );
+       } );
+
+}( mediaWiki, jQuery ) );

-- 
To view, visit https://gerrit.wikimedia.org/r/357124
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I066c33a01770b7d8026aa3e039af5fe5d9c4cdf9
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Mooeypoo <mor...@gmail.com>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to