diff --git a/src/client/Client.js b/src/client/Client.js
index 75fcc3b..04b84fb 100644
--- a/src/client/Client.js
+++ b/src/client/Client.js
@@ -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,13 @@ module.exports = Class( 'Client' )
this.ui = this._createUi( this.nav );
+ this._factory.createDataApiMediator(
+ this.ui,
+ this._dataValidator,
+ this.program.dapimap,
+ () => this.getQuote()
+ ).monitor( this._dapiManager );
+
this._eventHandler = this._factory.createClientEventHandler(
this, this._dataValidator, this.elementStyler, this.dataProxy, jQuery
);
@@ -756,11 +769,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 +792,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 );
diff --git a/src/client/ClientDependencyFactory.js b/src/client/ClientDependencyFactory.js
index 2ca86eb..73c8e7f 100644
--- a/src/client/ClientDependencyFactory.js
+++ b/src/client/ClientDependencyFactory.js
@@ -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,
} );
diff --git a/src/client/dapi/DataApiMediator.js b/src/client/dapi/DataApiMediator.js
new file mode 100644
index 0000000..bdca195
--- /dev/null
+++ b/src/client/dapi/DataApiMediator.js
@@ -0,0 +1,315 @@
+/**
+ * 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 .
+ */
+
+'use strict';
+
+const { Class } = require( 'easejs' );
+const MissingDataError = require( '../../dapi/MissingDataError' );
+
+
+/**
+ * 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,
+
+ /**
+ * DataAPI source/destination field map
+ * @type {Object}
+ */
+ 'private _dapi_map': 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.
+ *
+ * DAPI_MAP stores destination:source field mappings, where source is
+ * the result of the DataAPI call and destination is the target field in
+ * which to store those data.
+ *
+ * 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 {Object} dapi_map field source and destination map
+ * @param {function():Quote} quotef nullary function returning quote
+ */
+ constructor( ui, data_validator, dapi_map, quotef )
+ {
+ if ( typeof dapi_map !== 'object' )
+ {
+ throw TypeError( "dapi_map must be a key/value object" );
+ }
+
+ this._ui = ui;
+ this._data_validator = data_validator;
+ this._dapi_map = dapi_map;
+ 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, dapi_manager ) )
+ );
+
+ 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 {DataApiManager} dapi_manager DataAPI manager
+ * @param {string} name field name
+ * @param {number} index field index
+ * @param {Object} val_label value and label
+ * @param {Object} results DataAPI result set
+ *
+ * @return {undefined}
+ */
+ 'private _updateFieldData'( dapi_manager, 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 field_update = indexes.map( ( _, i ) =>
+ ( results[ existing[ i ] ] )
+ ? existing[ i ]
+ : this._getDefaultValue( val_label )
+ );
+
+ indexes.forEach( ( _, i ) =>
+ group.setOptions( name, i, val_label, existing[ i ] )
+ );
+
+
+ const update = this._populateWithMap(
+ dapi_manager, name, indexes, quote
+ );
+
+ update[ name ] = field_update;
+
+ quote.setData( update );
+ },
+
+
+ /**
+ * Generate bucket update with field expansion data
+ *
+ * If multiple indexes are provided, updates will be merged. If
+ * expansion data are missing, then the field will be ignored. If a
+ * destination field is populated such that auto-expanding would
+ * override that datum, then that field will be excluded from the
+ * expansion.
+ *
+ * @param {DataApiManager} dapi_manager manager responsible for fields
+ * @param {string} name field name
+ * @param {Array} indexes field indexes
+ * @param {Quote} quote source quote
+ *
+ * @return {undefined}
+ */
+ 'private _populateWithMap'( dapi_manager, name, indexes, quote )
+ {
+ const map = this._dapi_map[ name ];
+
+ // calculate field expansions for each index, which contains an
+ // object suitable as-is for use with Quote#setData
+ const expansions = indexes.map( ( _, i ) =>
+ {
+ try
+ {
+ return dapi_manager.getDataExpansion(
+ name, i, quote, map, false, {}
+ );
+ }
+ catch ( e )
+ {
+ if ( e instanceof MissingDataError )
+ {
+ // this value is ignored below
+ return undefined;
+ }
+
+ throw e;
+ }
+ } );
+
+ // produce a final update that merges each of the expansions
+ return expansions.reduce( ( update, expansion, i ) =>
+ {
+ // it's important that we check here instead of using #filter on
+ // the array so that we maintain index association
+ if ( expansion === undefined )
+ {
+ return update;
+ }
+
+ // merge each key individually
+ Object.keys( expansion ).forEach( key =>
+ {
+ const existing = ( quote.getDataByName( key ) || [] )[ i ];
+
+ // if set and non-empty, then it's already populated and we
+ // must leave the value alone (so as not to override
+ // something the user directly entered)
+ if ( existing !== undefined && existing !== "" )
+ {
+ return;
+ }
+
+ update[ key ] = update[ key ] || [];
+ update[ key ][ i ] = expansion[ key ][ i ];
+ } );
+
+ return update;
+ }, {} );
+ },
+
+
+ /**
+ * Clear field options
+ *
+ * @param {DataApiManager} dapi_manager DataAPI manager
+ * @param {string} name field name
+ * @param {number} index field index
+ *
+ * @return {undefined}
+ */
+ 'private _clearFieldOptions'( dapi_manager, name, index )
+ {
+ const group = this._ui.getCurrentStep().getElementGroup( name );
+
+ // ignore unknown fields
+ if ( group === undefined )
+ {
+ return;
+ }
+
+ group.clearOptions( name, index );
+ },
+
+
+ /**
+ * Clear field failures
+ *
+ * @param {DataApiManager} dapi_manager DataAPI manager
+ * @param {string} name field name
+ * @param {number} index field index
+ *
+ * @return {undefined}
+ */
+ 'private _clearFieldFailures'( dapi_manager, 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;
+ },
+} );
diff --git a/src/dapi/DataApiManager.js b/src/dapi/DataApiManager.js
index 9679058..73e8343 100644
--- a/src/dapi/DataApiManager.js
+++ b/src/dapi/DataApiManager.js
@@ -1,7 +1,7 @@
/**
* Manages DataAPI requests and return data
*
- * Copyright (C) 2016 R-T Specialty, LLC.
+ * Copyright (C) 2016, 2018 R-T Specialty, LLC.
*
* This file is part of the Liza Data Collection Framework
*
@@ -19,8 +19,9 @@
* along with this program. If not, see .
*/
-var Class = require( 'easejs' ).Class,
- EventEmitter = require( 'events' ).EventEmitter;
+const { Class } = require( 'easejs' );
+const { EventEmitter } = require( 'events' );
+const MissingDataError = require( './MissingDataError' );
/**
@@ -613,14 +614,14 @@ module.exports = Class( 'DataApiManager' )
)
{
var field_data = ( this._fieldData[ name ] || {} )[ index ],
- data = {};
+ data = {},
field_value = ( diff[ name ] || bucket.getDataByName( name ) )[ index ];
// if it's undefined, then the change probably represents a delete
if ( field_value === undefined )
{
( this._fieldDataEmitted[ name ] || [] )[ index ] = false;
- return;
+ return {};
}
// if we have no field data, try the "combined" index
@@ -638,9 +639,9 @@ module.exports = Class( 'DataApiManager' )
if ( !predictive && !( data ) && ( field_value !== '' ) )
{
// hmm..that's peculiar.
- this.emit( 'error', Error(
+ throw MissingDataError(
'Data missing for field ' + name + '[' + index + ']!'
- ) );
+ );
}
else if ( !data )
{
diff --git a/src/dapi/MissingDataError.js b/src/dapi/MissingDataError.js
new file mode 100644
index 0000000..0c72611
--- /dev/null
+++ b/src/dapi/MissingDataError.js
@@ -0,0 +1,26 @@
+/**
+ * MissingDataError
+ *
+ * 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 .
+ */
+
+const { Class } = require( 'easejs' );
+
+
+module.exports = Class( 'MissingDataError' )
+ .extend( Error, {} );
diff --git a/test/client/dapi/DataApiMediatorTest.js b/test/client/dapi/DataApiMediatorTest.js
new file mode 100644
index 0000000..fee5f56
--- /dev/null
+++ b/test/client/dapi/DataApiMediatorTest.js
@@ -0,0 +1,525 @@
+/**
+ * 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 .
+ */
+
+'use strict';
+
+const { expect } = require( 'chai' );
+
+const {
+ client: { dapi: { DataApiMediator: Sut } },
+ dapi: { MissingDataError },
+} = require( '../../../' );
+
+
+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: { foo: [ "first", "second" ] },
+ expected: {
+ foo: [ "first" ],
+ dest1: [ "src1data" ],
+ dest2: [ "src2data" ],
+ },
+
+ val_label: [
+ { value: "first result", label: "first" },
+ ],
+
+ results: {
+ first: { src1: "src1data", src2: "src2data" },
+ second: {},
+ },
+
+ expansion: [ {
+ dest1: [ "src1data" ],
+ dest2: [ "src2data" ],
+ } ],
+ },
+ {
+ label: "keeps existing value if in result set (second index)",
+ name: 'bar',
+ index: 1,
+ value: { bar: [ "first", "second" ] },
+ expected: {
+ bar: [ , "second" ],
+ dest1: [ , "src1data_2" ],
+ dest2: [ , "src2data_2" ],
+ },
+
+ val_label: [
+ { value: "first result", label: "first" },
+ { value: "second result", label: "second" },
+ ],
+
+ results: {
+ first: {},
+ second: { src1: "src1data_2", src2: "src2data_2" },
+ },
+
+ expansion: [ , {
+ dest1: [ , "src1data_2" ],
+ dest2: [ , "src2data_2" ],
+ } ],
+ },
+ {
+ label: "keeps existing value if in result set (all indexes)",
+ name: 'bar',
+ index: -1,
+ value: { bar: [ "first", "second" ] },
+ expected: {
+ bar: [ "first", "second" ],
+ dest1: [ "src1data", "src1data_2" ],
+ dest2: [ "src2data", "src2data_2" ],
+ },
+
+ val_label: [
+ { value: "first result", label: "first" },
+ { value: "second result", label: "second" },
+ ],
+
+ results: {
+ first: { src1: "src1data", src2: "src2data" },
+ second: { src1: "src1data_2", src2: "src2data_2" },
+ },
+
+ expansion: [
+ {
+ dest1: [ "src1data" ],
+ dest2: [ "src2data" ],
+ },
+ {
+ dest1: [ , "src1data_2" ],
+ dest2: [ , "src2data_2" ],
+ },
+ ],
+ },
+
+ {
+ label: "uses first value of result if existing not in result set (first index)",
+ name: 'foo',
+ index: 0,
+ value: { foo: [ "does not", "exist" ] },
+ expected: {
+ foo: [ "first result" ],
+ desta: [ "src1data" ],
+ destb: [ "src2data" ],
+ },
+
+ val_label: [
+ { value: "first result", label: "first" },
+ { value: "second result", label: "second" },
+ ],
+
+ results: {
+ first: { src1: "src1data", src2: "src2data" },
+ second: {},
+ },
+
+ expansion: [ {
+ desta: [ "src1data" ],
+ destb: [ "src2data" ],
+ } ],
+ },
+ {
+ label: "uses first value of result if existing not in result set (second index)",
+ name: 'foo',
+ index: 1,
+ value: { foo: [ "does not", "exist" ] },
+ expected: {
+ foo: [ , "first result" ],
+ desta: [ , "src1data" ],
+ destb: [ , "src2data" ],
+ },
+
+ val_label: [
+ { value: "first result", label: "first" },
+ { value: "second result", label: "second" },
+ ],
+
+ results: {
+ first: { src1: "src1data", src2: "src2data" },
+ second: {},
+ },
+
+ expansion: [ , {
+ desta: [ , "src1data" ],
+ destb: [ , "src2data" ],
+ } ],
+ },
+ {
+ label: "uses first value of result if existing not in result set (all indexes)",
+ name: 'foo',
+ index: -1,
+ value: { foo: [ "does not", "exist" ] },
+ expected: {
+ foo: [ "first result", "first result" ],
+ desta: [ "src1data", "src1data" ],
+ destb: [ "src1data", "src2data" ],
+ },
+
+ val_label: [
+ { value: "first result", label: "first" },
+ { value: "second result", label: "second" },
+ ],
+
+ results: {
+ first: { src1: "src1data", src2: "src2data" },
+ second: {},
+ },
+
+ expansion: [
+ {
+ desta: [ "src1data" ],
+ destb: [ "src1data" ],
+ },
+ {
+ desta: [ , "src1data" ],
+ destb: [ , "src2data" ],
+ },
+ ],
+ },
+
+ {
+ label: "uses empty string if empty result set (first index)",
+ name: 'foo',
+ index: 0,
+ value: { foo: [ "foo" ] },
+ expected: {
+ foo: [ "" ],
+ dest1: [ "" ],
+ },
+
+ val_label: [],
+ results: {},
+ expansion: [ {
+ dest1: [ "" ],
+ } ],
+ },
+ {
+ label: "uses empty string if empty result set (second index)",
+ name: 'foo',
+ index: 1,
+ value: { foo: [ "foo", "bar" ] },
+ expected: {
+ foo: [ , "" ],
+ dest1: [ , "" ],
+ },
+
+ val_label: [],
+ results: {},
+ expansion: [ , {
+ dest1: [ , "" ],
+ } ],
+ },
+ {
+ label: "uses empty string if empty result set (all indexes)",
+ name: 'foo',
+ index: -1,
+ value: { foo: [ "foo", "bar" ] },
+ expected: {
+ foo: [ "", "" ],
+ dest1: [ "", "" ],
+ dest2: [ "", "" ],
+ },
+
+ val_label: [],
+ results: {},
+ expansion: [
+ {
+ dest1: [ "" ],
+ dest2: [ "" ],
+ },
+ {
+ dest1: [ , "" ],
+ dest2: [ , "" ],
+ },
+ ],
+ },
+
+ {
+ label: "does not auto-expand into non-empty fields",
+ name: 'foo',
+ index: 0,
+ value: {
+ foo: [ "first", "second" ],
+ dest1: [ "leave alone" ],
+ dest2: [ "" ],
+ },
+ expected: {
+ foo: [ "first" ],
+ // dest1 missing because it is already populated
+ dest2: [ "src2data" ],
+ },
+
+ val_label: [
+ { value: "first result", label: "first" },
+ ],
+
+ results: {
+ first: { src1: "src1data", src2: "src2data" },
+ second: {},
+ },
+
+ expansion: [ {
+ dest1: [ "src1data" ],
+ dest2: [ "src2data" ],
+ } ],
+ }
+ ].forEach( ( {
+ label, name, index, value, expected, val_label, results, expansion
+ } ) =>
+ {
+ it( label, done =>
+ {
+ let set_options = false;
+
+ const quote = {
+ getDataByName( given_name )
+ {
+ return value[ given_name ];
+ },
+
+ setData( given_data )
+ {
+ expect( given_data ).to.deep.equal( expected );
+
+ // should have called setOptions by now
+ expect( set_options ).to.be.true;
+
+ done();
+ },
+ };
+
+ const getQuote = () => quote;
+
+ const dapi_manager = createStubDapiManager( expansion );
+
+ // this isn't a valid map, but comparing the objects will
+ // ensure that the map is actually used
+ const dapimap = { foo: {}, bar: {} };
+
+ dapi_manager.getDataExpansion = (
+ given_name, given_index, given_quote, given_map,
+ predictive, diff
+ ) =>
+ {
+ expect( given_name ).to.equal( name );
+ expect( given_quote ).to.equal( quote );
+ expect( given_map ).to.deep.equal( dapimap );
+ expect( predictive ).to.be.false;
+ expect( diff ).to.deep.equal( {} );
+
+ return expansion[ given_index ];
+ };
+
+ 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_name ][ given_index ] );
+
+ set_options = true;
+ },
+ },
+ };
+
+ const ui = createStubUi( field_groups );
+
+ const sut = Sut( ui, {}, { [name]: dapimap }, getQuote )
+ .monitor( dapi_manager );
+
+ dapi_manager.emit(
+ 'updateFieldData', name, index, val_label, results
+ );
+ } );
+ } );
+
+
+ it( 'does not perform expansion if data are not available', done =>
+ {
+ const dapi_manager = createStubDapiManager();
+
+ dapi_manager.getDataExpansion = () =>
+ {
+ throw MissingDataError(
+ 'this should happen, but should be caught'
+ );
+ };
+
+ const name = 'foo';
+ const value = 'bar';
+
+ const getQuote = () => ( {
+ getDataByName: () => [ value ],
+ setData( given_data )
+ {
+ // only the value should be set with no expansion data
+ expect( given_data ).to.deep.equal( {
+ [name]: [ value ],
+ } );
+
+ done();
+ },
+ } );
+
+ const field_groups = { [name]: { setOptions() {} } };
+
+ const ui = createStubUi( field_groups );
+ const sut = Sut( ui, {}, {}, getQuote ).monitor( dapi_manager );
+
+ const val_label = [
+ { value: value, label: "bar" },
+ ];
+
+ dapi_manager.emit( 'updateFieldData', name, 0, val_label, {} );
+ } );
+ } );
+
+
+ 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 ]
+ } )
+ };
+}