1
0
Fork 0

DataApiMediator: New class

This extracts existing code from Client and adds tests.  The glue code is
far from ideal and highlights the amount of work needed to decouple Client
from so many parts of the system.

* src/client/Client.js (_dapiManager): New field.
  (_init): Use DataApiMediator.
  (_createProgram): Assign `_dapiManager' (this is not at all
  ideal).  Remove hooks from it: fieldLoading, updateFieldData,
  clearFieldData.
* src/client/ClientDependencyFactory.js (createDataApiMediator): New alias
  to DataApiMediator constructor.
* src/client/dapi/DataApiMediator.js: New class.
* test/client/dapi/DataApiMediatorTest.js: New test case.

DEV-3257
master
Mike Gerwitz 2018-07-03 09:37:59 -04:00
parent 3261fbd7ec
commit e25bec5ac0
4 changed files with 551 additions and 94 deletions

View File

@ -239,6 +239,12 @@ module.exports = Class( 'Client' )
*/
'private _rootContext': null,
/**
* DataApi Manager
* @type {DataApiManager}
*/
'private _dapiManager': null,
/**
* User-visible validation error messages
* @type {Object}
@ -338,6 +344,12 @@ module.exports = Class( 'Client' )
this.ui = this._createUi( this.nav );
this._factory.createDataApiMediator(
this.ui,
this._dataValidator,
() => this.getQuote()
).monitor( this._dapiManager );
this._eventHandler = this._factory.createClientEventHandler(
this, this._dataValidator, this.elementStyler, this.dataProxy, jQuery
);
@ -756,11 +768,11 @@ module.exports = Class( 'Client' )
try
{
var dapi_manager = this._factory.createDataApiManager();
this._dapiManager = this._factory.createDataApiManager();
var program = this._factory.createProgram(
this.programId,
dapi_manager
this._dapiManager
);
}
catch ( e )
@ -779,98 +791,7 @@ module.exports = Class( 'Client' )
} );
// handle field updates
dapi_manager
.on( 'fieldLoading', function( name, index )
{
var group = _self.getUi().getCurrentStep().getElementGroup(
name
);
if ( !group )
{
return;
}
// -1 represents "all indexes"
if ( index === -1 )
{
index = undefined;
}
} )
.on( 'updateFieldData', function( name, index, data, fdata )
{
var group = _self.getUi().getCurrentStep().getElementGroup(
name
);
if ( !group )
{
return;
}
var cur_data = _self._quote.getDataByName( name );
if ( +index === -1 )
{
// -1 is the "combined" index, representing every field
indexes = cur_data;
}
else
{
indexes = [];
indexes[ index ] = index;
}
var update = [];
for ( var i in indexes )
{
var cur = undefined;
if ( data.length )
{
cur = cur_data[ i ];
update[ i ] = ( fdata[ cur ] )
? cur
: data[ 0 ].value;
}
else
{
update[ i ] = '';
}
// populate and enable field *only if* results were returned
// and if the quote has not been locked; but first, give the
// UI a chance to finish updating
( function( index, cur )
{
setTimeout( function()
{
group.setOptions( name, index, data, cur );
}, 0 );
} )( i, cur );
}
update.length && _self._quote.setDataByName( name, update );
} )
.on( 'clearFieldData', function( name, index )
{
if ( !_self.getUi().getCurrentStep().getElementGroup( name ) )
{
return;
}
// clear and disable the field (if there's no value, then there
// is no point in allowing them to do something with it)
_self.getUi().getCurrentStep().getElementGroup( name )
.clearOptions( name, index );
} )
.on( 'fieldLoaded', ( name, index ) =>
{
_self._dataValidator.clearFailures( {
[name]: [ index ],
} );
} )
this._dapiManager
.on( 'error', function( e )
{
_self.handleError( e );

View File

@ -97,6 +97,8 @@ var Step = require( '../step/Step' ),
diffStore = require( 'liza/system/client' ).data.diffStore,
DataApiMediator = require( './dapi/DataApiMediator' ),
Class = require( 'easejs' ).Class;
@ -371,4 +373,6 @@ module.exports = Class( 'ClientDependencyFactory',
'action$cvv2Dialog': requireh( 'Cvv2DialogEventHandler' )( jquery )
} );
},
createDataApiMediator: DataApiMediator,
} );

View File

@ -0,0 +1,213 @@
/**
* Data API mediator
*
* Copyright (C) 2018 R-T Specialty, LLC.
*
* This file is part of the Liza Data Collection Framework
*
* Liza is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
const { Class } = require( 'easejs' );
/**
* Mediate updates to system state based on DataAPI request status and
* results
*
* The UI will be updated to reflect the options returned by DataAPI
* requests. When a field is cleared of all options, any errors on that
* field will be cleared.
*/
module.exports = Class( 'DataApiMediator',
{
/**
* UI
* @type {Ui}
*/
'private _ui': null,
/**
* Data validator for clearing failures
* @type {DataValidator}
*/
'private _data_validator': null,
/**
* Function returning active quote
* @type {function():Quote}
*/
'private _quotef': null,
/**
* Initialize mediator
*
* The provided DataValidator DATA_VALIDATOR must be the same validator
* used to produce errors on fields to ensure that its state can be
* appropriately cleared.
*
* Since the active quote changes at runtime, this constructor accepts a
* quote function QUOTEF to return the active quote.
*
* @param {Ui} ui UI
* @param {DataValidator} data_validator data validator
* @param {function():Quote} quotef nullary function returning quote
*/
constructor( ui, data_validator, quotef )
{
this._ui = ui;
this._data_validator = data_validator;
this._quotef = quotef;
},
/**
* Hook given DataApiManager
*
* Handled events are updateFieldData, clearFieldData, and fieldLoaded.
*
* @param {DataApiManager} dapi_manager manager to hook
*
* @return {DataApiMediator} self
*/
'public monitor'( dapi_manager )
{
const handlers = [
[ 'updateFieldData', this._updateFieldData ],
[ 'clearFieldData', this._clearFieldOptions ],
[ 'fieldLoaded', this._clearFieldFailures ],
]
handlers.forEach( ( [ event, handler ] ) =>
dapi_manager.on( event, handler.bind( this ) )
);
return this;
},
/**
* Set field options
*
* If the bucket value associated with NAME and INDEX are in the result
* set RESULTS, then it will be selected. Otherwise, the first result
* in RESULTS will be selected, if any. If there are no results in
* RESULTS, the set value will be the empty string.
*
* @param {string} name field name
* @param {number} index field index
* @param {Object<value,label>} val_label value and label
* @param {Object} results DataAPI result set
*
* @return {undefined}
*/
'private _updateFieldData'( name, index, val_label, results )
{
const group = this._ui.getCurrentStep().getElementGroup( name );
if ( !group )
{
return;
}
const quote = this._quotef();
const existing = quote.getDataByName( name ) || [];
let indexes = [];
// index of -1 indicates that all indexes should be affected
if ( index === -1 )
{
indexes = existing;
}
else
{
indexes[ index ] = index;
}
// keep existing value if it exists in the result set, otherwise
// use the first value of the set
const update = indexes.map( ( _, i ) =>
( results[ existing[ i ] ] )
? existing[ i ]
: this._getDefaultValue( val_label )
);
indexes.forEach( ( _, i ) =>
group.setOptions( name, i, val_label, existing[ i ] )
);
quote.setDataByName( name, update );
},
/**
* Clear field options
*
* @param {string} name field name
* @param {number} index field index
*
* @return {undefined}
*/
'private _clearFieldOptions'( name, index )
{
const group = this._ui.getCurrentStep().getElementGroup( name );
// ignore unknown fields
if ( group === undefined )
{
return;
}
group.clearOptions( name, index );
},
/**
* Clear field failures
*
* @param {string} name field name
* @param {number} index field index
*
* @return {undefined}
*/
'private _clearFieldFailures'( name, index )
{
this._data_validator.clearFailures( {
[name]: [ index ],
} );
},
/**
* Determine default value for result set
*
* @param {Object} val_label value and label
*
* @return {string} default value for result set
*/
'private _getDefaultValue'( val_label )
{
// default to the empty string if no results were returned
if ( val_label.length === 0 )
{
return "";
}
return ( val_label[ 0 ] || {} ).value;
},
} );

View File

@ -0,0 +1,319 @@
/**
* Tests DataApiMediator
*
* Copyright (C) 2018 R-T Specialty, LLC.
*
* This file is part of the Liza Data Collection Framework
*
* Liza is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
const { expect } = require( 'chai' );
const Sut = require( '../../../' ).client.dapi.DataApiMediator;
describe( "DataApiMediator", () =>
{
it( "returns self on #monitor", () =>
{
const dapi_manager = createStubDapiManager();
const sut = Sut( {}, {}, {} );
expect( sut.monitor( dapi_manager ) ).to.equal( sut );
} );
describe( "updateFieldData event", () =>
{
it( "ignores unknown fields", () =>
{
const dapi_manager = createStubDapiManager();
const getQuote = () => ( {
getDataByName: () => {},
setDataByName: () => {},
} );
const ui = createStubUi( {} ); // no field groups
const sut = Sut( ui, {}, getQuote ).monitor( dapi_manager );
dapi_manager.emit( 'updateFieldData', '', 0, {}, {} );
} );
[
{
label: "keeps existing value if in result set (first index)",
name: 'foo',
index: 0,
value: [ "first", "second" ],
expected: [ "first" ],
val_label: [
{ value: "first result", label: "first" },
],
results: { first: {}, second: {} },
},
{
label: "keeps existing value if in result set (second index)",
name: 'bar',
index: 1,
value: [ "first", "second" ],
expected: [ , "second" ],
val_label: [
{ value: "first result", label: "first" },
{ value: "second result", label: "second" },
],
results: { first: {}, second: {} },
},
{
label: "keeps existing value if in result set (all indexes)",
name: 'bar',
index: -1,
value: [ "first", "second" ],
expected: [ "first", "second" ],
val_label: [
{ value: "first result", label: "first" },
{ value: "second result", label: "second" },
],
results: { first: {}, second: {} },
},
{
label: "uses first value of result if existing not in result set (first index)",
name: 'foo',
index: 0,
value: [ "does not", "exist" ],
expected: [ "first result" ],
val_label: [
{ value: "first result", label: "first" },
{ value: "second result", label: "second" },
],
results: {},
},
{
label: "uses first value of result if existing not in result set (second index)",
name: 'foo',
index: 1,
value: [ "does not", "exist" ],
expected: [ , "first result" ],
val_label: [
{ value: "first result", label: "first" },
{ value: "second result", label: "second" },
],
results: {},
},
{
label: "uses first value of result if existing not in result set (all indexes)",
name: 'foo',
index: -1,
value: [ "does not", "exist" ],
expected: [ "first result", "first result" ],
val_label: [
{ value: "first result", label: "first" },
{ value: "second result", label: "second" },
],
results: {},
},
{
label: "uses empty string if empty result set (first index)",
name: 'foo',
index: 0,
value: [ "foo" ],
expected: [ "" ],
val_label: [],
results: {},
},
{
label: "uses empty string if empty result set (second index)",
name: 'foo',
index: 1,
value: [ "foo", "bar" ],
expected: [ , "" ],
val_label: [],
results: {},
},
{
label: "uses empty string if empty result set (all indexes)",
name: 'foo',
index: -1,
value: [ "foo", "bar" ],
expected: [ "", "" ],
val_label: [],
results: {},
},
].forEach( ( { label, name, index, value, expected, val_label, results }, i ) =>
{
it( label, done =>
{
let set_options = false;
const getQuote = () => ( {
getDataByName( given_name )
{
expect( given_name ).to.equal( name );
return value;
},
setDataByName( given_name, given_data )
{
expect( given_name ).to.equal( name );
expect( given_data ).to.deep.equal( expected );
// should have called setOptions by now
expect( set_options ).to.be.true;
done();
},
} );
const dapi_manager = createStubDapiManager();
const field_groups = {
[name]: {
setOptions( given_name, given_index, given_data, given_cur )
{
// index is implicitly tested by the given_cur line
expect( given_name ).to.equal( name );
expect( given_data ).to.deep.equal( val_label );
expect( given_cur ).to.equal( value[ given_index ] );
set_options = true;
},
},
};
const ui = createStubUi( field_groups );
const sut = Sut( ui, {}, getQuote ).monitor( dapi_manager );
dapi_manager.emit(
'updateFieldData', name, index, val_label, results
);
} );
} );
} );
describe( "on clearFieldData event", () =>
{
it( "ignores unknown fields", () =>
{
const dapi_manager = createStubDapiManager();
const field_groups = {}; // no groups
const ui = createStubUi( field_groups );
const sut = Sut( ui ).monitor( dapi_manager );
dapi_manager.emit( 'clearFieldData', 'unknown', 0 );
} );
it( "clears field", done =>
{
const dapi_manager = createStubDapiManager();
const name = 'foo';
const index = 3;
const field_groups = {
[name]: {
clearOptions( given_name, given_index )
{
expect( given_name ).to.equal( name );
expect( given_index ).to.equal( index );
done();
},
},
};
const ui = createStubUi( field_groups );
const sut = Sut( ui ).monitor( dapi_manager );
dapi_manager.emit( 'clearFieldData', name, index );
} );
} );
describe( "on fieldLoaded event", () =>
{
it( "clears failures for field", done =>
{
const dapi_manager = createStubDapiManager();
const name = 'bar';
const index = 2;
const data_validator = {
clearFailures( data )
{
expect( data[ name ] ).to.deep.equal( [ index ] );
done();
},
};
const ui = {}; // unused by this event
const sut = Sut( ui, data_validator ).monitor( dapi_manager );
dapi_manager.emit( 'fieldLoaded', name, index );
} );
} );
} );
function createStubDapiManager()
{
const callbacks = {};
return {
on( name, callback )
{
callbacks[ name ] = callback;
},
emit( name )
{
// we don't support rest yet in our version of node
const data = Array.prototype.slice.call( arguments, 1 );
callbacks[ name ].apply( null, data );
},
};
}
function createStubUi( field_groups )
{
return {
getCurrentStep: () => ( {
getElementGroup: name => field_groups[ name ]
} )
};
}