Tobias Gritschacher has submitted this change and it was merged. Change subject: Added test for valueview experts in general and StringValue expert specifically ......................................................................
Added test for valueview experts in general and StringValue expert specifically Introduces jQuery.valueview.tests.testExpert which will test a Expert constructor on basis of a given test definition. Added first expert test using this new function on the StringValue expert. Fixes some minor issues and updates documentation of jQuery.valueview.Expert itself to pass the new tests. Change-Id: Ia681d6a8a31771cc000d5f1f423de25a77f6a6c2 --- M ValueView/ValueView.tests.qunit.php M ValueView/resources/jquery.valueview/valueview.Expert.js A ValueView/tests/qunit/jquery.valueview/valueview.experts/experts.StringValue.tests.js A ValueView/tests/qunit/jquery.valueview/valueview.tests.testExpert.js 4 files changed, 437 insertions(+), 8 deletions(-) Approvals: Tobias Gritschacher: Looks good to me, approved jenkins-bot: Verified diff --git a/ValueView/ValueView.tests.qunit.php b/ValueView/ValueView.tests.qunit.php index 81ebd1a..25606d3 100644 --- a/ValueView/ValueView.tests.qunit.php +++ b/ValueView/ValueView.tests.qunit.php @@ -82,6 +82,25 @@ 'qunit.parameterize', ), ), + + 'jquery.valueview.tests.testExpert.js' => array( + 'scripts' => array( + "$bp/jquery.valueview/valueview.tests.testExpert.js", + ), + 'dependencies' => array( + 'jquery.valueview.experts', + 'qunit.parameterize', + ), + ), + + 'jquery.valueview.experts.stringvalue.tests' => array( + 'scripts' => array( + "$bp/jquery.valueview/valueview.experts/experts.StringValue.tests.js", + ), + 'dependencies' => array( + 'jquery.valueview.experts.stringvalue', + ), + ), ); } ); diff --git a/ValueView/resources/jquery.valueview/valueview.Expert.js b/ValueView/resources/jquery.valueview/valueview.Expert.js index 4aed814..4579fc6 100644 --- a/ValueView/resources/jquery.valueview/valueview.Expert.js +++ b/ValueView/resources/jquery.valueview/valueview.Expert.js @@ -62,7 +62,8 @@ * @param {dv.util.Notifier} [valueViewNotifier] Required so the expert can notify the valueview * about certain events. The following notification keys can be used: * - change: will be sent when raw value displayed by the expert changes. Either by a - * user action or by calling the rawValue() method. + * user action or by calling the rawValue() method. First parameter is a + * reference to the Expert itself. * @param {Object} [options={}] * * TODO: think about whether there should be a function to add multiple notifiers for widget @@ -167,7 +168,7 @@ * @since 0.1 * @abstract * - * @return valueParsers.Parser + * @return valueParsers.ValueParser */ parser: function() { return new vp.NullParser() @@ -194,7 +195,8 @@ * value, so basically, the expert itself will do the job of parsing itself somehow and does * not require an asynchronous job for doing so. * If the first parameter is set, then the function will set the value instead of returning - * it. An incompatible value will be recognized as empty (same as null). + * it. An incompatible value will be recognized as empty (same as null). If the given value + * is different from the current one, "change" will be notified to the change notifier. * * @since 0.1 * @@ -203,13 +205,24 @@ * Returns undefined if used as setter. */ rawValue: function( rawValue ) { + var currentRawValue = this._getRawValue(); + if( rawValue === undefined ) { // GETTER: - return this._getRawValue(); + return currentRawValue; } - // SETTER: - this._setRawValue( rawValue ); - this.draw(); - this._viewNotifier.notify( 'change' ); + + // Only change value if different from current value: + if( !this.rawValueCompare( currentRawValue, rawValue ) ) { // SETTER: + this._setRawValue( rawValue ); + + // rawValue might be a unknown value different from null which will end as null + // nonetheless. If that is the case the value was null already, then this is not + // a real update. + if( currentRawValue !== null || this._getRawValue() !== null ) { + this.draw(); + this._viewNotifier.notify( 'change', [ this ] ); + } + } }, /** diff --git a/ValueView/tests/qunit/jquery.valueview/valueview.experts/experts.StringValue.tests.js b/ValueView/tests/qunit/jquery.valueview/valueview.experts/experts.StringValue.tests.js new file mode 100644 index 0000000..1a8aa9f --- /dev/null +++ b/ValueView/tests/qunit/jquery.valueview/valueview.experts/experts.StringValue.tests.js @@ -0,0 +1,25 @@ +/** + * @since 0.1 + * @ingroup ValueView + * + * @licence GNU GPL v2+ + * @author Daniel Werner < daniel.wer...@wikimedia.de > + */ + ( function( $, QUnit, valueview, StringParser ) { + + 'use strict'; + + var testExpert = valueview.tests.testExpert; + + QUnit.module( 'jquery.valueview.experts.StringValue' ); + + testExpert( { + expertConstructor: valueview.experts.StringValue, + rawValues: { + valid: [ 'foo bar', '42', '*(&#$@#*$' ], + unknown: testExpert.basicTestDefinition.rawValues.unknown.concat( [ 42 ] ) + }, + relatedValueParser: StringParser + } ); + +}( jQuery, QUnit, jQuery.valueview, valueParsers.StringParser ) ); diff --git a/ValueView/tests/qunit/jquery.valueview/valueview.tests.testExpert.js b/ValueView/tests/qunit/jquery.valueview/valueview.tests.testExpert.js new file mode 100644 index 0000000..32d1ef3 --- /dev/null +++ b/ValueView/tests/qunit/jquery.valueview/valueview.tests.testExpert.js @@ -0,0 +1,372 @@ +/** + * @since 0.1 + * @ingroup ValueView + * + * @licence GNU GPL v2+ + * @author Daniel Werner < daniel.wer...@wikimedia.de > + */ + jQuery.valueview.tests = jQuery.valueview.tests || {}; + jQuery.valueview.tests.testExpert = ( function( $, QUnit, valueview, Notifier, ValueParser ) { + +'use strict'; + + /** + * Helper which returns a string describing some value in more detail. The string will hold a + * quoted representation of the value and a note of what type the value is. + * + * @param {*} value + * @return string + */ +function valueDescription( value ) { + return '"' + value + '" (' + Object.prototype.toString.call( value ) + ')'; +} + + /** + * Tests different aspects of a valueview expert. + * @since 0.1 + * + * TODO: Test error cases. + * + * @param {Object} testDefinition See testExpert.basicTestDefinition for documentation. + */ +function testExpert( testDefinition ) { + // Throw error if something is wrong with given test definition: + testExpert.verifyTestDefinition( testDefinition ); + + var Expert = testDefinition.expertConstructor, + validRawValues = testDefinition.rawValues.valid, + validRawValue = validRawValues[0], + unknownRawValues = testDefinition.rawValues.unknown, + unknownRawValue = unknownRawValues[0]; + + // Add null (empty) to list of valid values: + if( $.inArray( null, validRawValues ) > 0 ) { + throw new Error( 'null should not be part of the list of valid values since it will be ' + + 'added by default' ); + } + validRawValues.push( null ); + + // Used as source for expertProvider(). + var expertDefinitions = [ + { + title: 'instance without notifier', + args: [ + $( '<span/>' ), + new valueview.MockViewState() + ] + }, { + title: 'instance with notifier', + args: [ + $( '<div/>' ), + new valueview.MockViewState(), + new Notifier() + ] + }, { + title: 'instance with ViewState of disabled view', + args: [ + $( '<div/>' ), + new valueview.MockViewState( { isDisabled: true } ), + new Notifier() + ] + } + ]; + + /** + * Returns an array of Objects holding the following fields: + * - title: Description of the set. + * - expert: A valueview Expert instance. + * - constructorArgs: Arguments used to construct the expert given in the "expert" field. + * + * @returns {Array} + */ + function expertProvider() { + var experts = []; + + $.each( expertDefinitions, function( i, definition ) { + var $viewPort = definition.args[0], + viewState = definition.args[1], + notifier = definition.args[2]; + + var expert = new Expert( $viewPort, viewState, notifier ); + + experts.push( { + title: definition.title, + expert: expert, + constructorArgs: definition.args + } ); + } ); + + return experts; + } + + var expertCases = QUnit.cases( expertProvider() ); + + expertCases.test( 'constructor', function( args, assert ) { + assert.ok( + args.expert instanceof Expert, + 'expert successfully constructed' + ); + assert.ok( + args.expert instanceof valueview.Expert, + 'expert instance of jQuery.valueview.Expert' + ); + } ); + + expertCases.test( 'parser', function( args, assert ) { + var valueParser = args.expert.parser(); + + assert.ok( + valueParser instanceof ValueParser, + 'parser() returns a value parser instance' + ); + assert.ok( + valueParser instanceof testDefinition.relatedValueParser, + 'parser() returns instance of the expected value parser constructor' + ); + } ); + + expertCases.test( 'viewState', function( args, assert ) { + var viewState = args.expert.viewState(); + assert.ok( + viewState instanceof valueview.ViewState, + 'viewState() returns a jQuery.valueview.ViewState instance' + ); + } ); + + expertCases.test( 'rawValueCompare: Test of different raw values', function( args, assert ) { + $.each( validRawValues, function( i, testValue ) { + $.each( validRawValues, function( j, otherValue ) { + var successExpected = i === j; + + assert.ok( + args.expert.rawValueCompare( testValue, otherValue ) === successExpected, + 'Raw value "' + valueDescription( testValue ) + ' does ' + + ( successExpected ? '' : 'not ' ) + 'equal raw value "' + + valueDescription( otherValue ) + ); + } ); + } ); + } ); + + expertCases.test( 'rawValueCompare: Works with 2nd parameter omitted', function( args, assert ) { + var expert = args.expert; + + assert.ok( + expert.rawValueCompare( null ), + '"rawValueCompare( null )" is true for newly initialized expert' + ); + + expert.rawValue( validRawValue ); + + assert.ok( + expert.rawValueCompare( validRawValue ), + '"rawValueCompare( value )" is true after "rawValue( value )"' + ); + } ); + + expertCases.test( 'rawValue: setting and getting raw value', function( args, assert ) { + var expert = args.expert; + + $.each( validRawValues, function( i, testValue ) { + expert.rawValue( testValue ); + + assert.ok( + true, + 'Changed value via "rawValue( value )". "value" is ' + valueDescription( testValue ) + ); + assert.ok( + expert.rawValueCompare( expert.rawValue(), testValue ), + 'The new value has been received via "rawValue()"' + ); + } ); + } ); + + expertCases.test( 'rawValue: setting value to unknown value', function( args, assert ) { + var expert = args.expert; + + $.each( unknownRawValues, function( i, testValue ) { + expert.rawValue( testValue ); + + assert.ok( + true, + 'Changed value via "rawValue( value )". "value" is ' + valueDescription( testValue ) + ); + assert.ok( + expert.rawValueCompare( expert.rawValue(), null ), + 'The new value returned by "rawValue()" is null (empty value)' + ); + } ); + } ); + + var expertCasesMemberCallTest = function( memberName ) { + expertCases.test( memberName, function( args, assert ) { + args.expert[ memberName ](); + assert.ok( + true, + memberName + '() has been called' + ); + } ); + }; + expertCasesMemberCallTest( 'draw' ); + expertCasesMemberCallTest( 'focus' ); + expertCasesMemberCallTest( 'blur' ); + + // Separate test for change notification: + QUnit.test( 'Expert change notification', 10, function( assert ) { + // Helper flags for tests: + var notified = false, + newValue; + + // Notifier with callback to set flag above after "change" notification. + var notifier = new Notifier( { + change: function( expert, arg2 ) { + notified = true; + assert.ok( + arguments.length === 1 && expert instanceof Expert, + '"change" notification received, first argument is expert' + ); + assert.ok( + expert.rawValueCompare( newValue, expert.rawValue() ), + 'Value has been changed to expected value' + ); + } + } ); + + var expert = new Expert( + $( '<div/>' ), + new valueview.MockViewState(), + notifier + ); + + assert.ok( // +1 + !notified, + 'Change notification has not been triggered initially' + ); + + function testChangeRawValue( rawValue, changeExptected, testDescription ) { + notified = false; + newValue = rawValue; + expert.rawValue( rawValue ); + + var msg = changeExptected + ? 'Changing the expert\'s raw value has triggered a "change" notification after ' + : 'No "change" notification has been triggered since the expert\'s value did not ' + + 'change after '; + msg += testDescription; + + assert.ok( changeExptected === notified, msg ); + } + + testChangeRawValue( null, false, 'changing value to empty after initialization ' + + '(value should be empty already)' ); // +1 + testChangeRawValue( validRawValue, true, 'changing value to valid value' ); // +3 assertions + testChangeRawValue( validRawValue, false, 'attempt to change value to current value' ); // +1 + testChangeRawValue( null, true, 'change value to empty value and not' ); // +3 + testChangeRawValue( unknownRawValue, false, 'changing value to unknown value which will ' + + 'be interpreted as empty value' ); // +1 + } ); +} + + /** + * Object holding all fields required by testExpert's first argument's object. + * @since 0.1 + * @type Object + */ +testExpert.basicTestDefinition = { + /** + * A jQuery.valueview.Expert implementation's constructor to be tested. + * @type Function + */ + expertConstructor: valueview.experts.StringValue, + /** + * Definition of different raw values. Holds different fields for different kinds of raw values. + * The keys "valid" and "unknown" should each hold at least one value in their array of values + * and the values must not have any duplicates. + * @type Object + */ + rawValues: { + /** + * Array of valid raw values. + * @type {*[]} + */ + valid: [], + /** + * Array of unknown raw values. These values are expected to be interpreted as "null" by the + * Expert. null must not be part of the list. + */ + unknown: [ + [], // array + {}, // plain object + new function NotSoPlainObject() {}(), // not-so-plain object + /regex/, // regex + $.noop, // function + Number['NaN'] // NaN + ] + }, + /** + * Defines what kind of value parser the Expert's parse() function is exptected to return. + * @type Function Constructor implementation of valueParsers.ValueParser. + */ + relatedValueParser: null +}; + + /** + * Will validate a test definition for the "testExpert". Throws an error if something is wrong with + * the given test definition. + * + * @param {Object} testDefinition + */ +testExpert.verifyTestDefinition = function( testDefinition ) { + function verifyTestDefinitionRawValues( rawValueDefinitions ) { + var allValues = [], + value, i; + + if( !$.isPlainObject( rawValueDefinitions ) + || !rawValueDefinitions.unknown + || !rawValueDefinitions.valid + ) { + throw new Error( 'Test definition\'s raw value definitions require on set of values for ' + + '"valid" and one set of values for "unknown" values' ); + } + + $.each( rawValueDefinitions, function( valuesType, setOfValues ) { + if( setOfValues.length < 1 ) { + throw new Error( 'Test definition\'s set of values "' + valuesType + '" has to be an ' + + 'array of at least one value representing the type of value' ); + } + allValues = allValues.concat( setOfValues ); + } ); + + for( i = allValues.length - 1; i >= 0; i-- ) { + value = allValues[i]; + if( value === undefined ) { + throw new Error( 'Expert test definition\'s sets of values given in the rawValue ' + + 'field must not contain undefined as a value' ); + } + if( + $.inArray( value, allValues ) < i + && ( + typeof value !== 'number' + || !isNaN( value ) // NaN might be a unknown value, but NaN !== NaN, $.inArray returns -1 + ) + ) { + throw new Error( 'Expert test definition\'s sets of values given in the rawValue ' + + 'field must be sets of unique values. E.g. a unknown value can not be a valid ' + + 'value at the same time. Value ' + valueDescription( value ) + ' is a duplicate' ); + } + } + } + + verifyTestDefinitionRawValues( testDefinition.rawValues ); + + if( !testDefinition.expertConstructor + || !( testDefinition.expertConstructor.prototype instanceof valueview.Expert ) + ) { + throw new Error( 'Test definition\'s "expertConstructor" field has to hold a constructor ' + + 'implementing jQuery.valueview.Expert' ); + } +}; + +return testExpert; // expose + +}( jQuery, QUnit, jQuery.valueview, dataValues.util.Notifier, valueParsers.ValueParser ) ); -- To view, visit https://gerrit.wikimedia.org/r/62765 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Ia681d6a8a31771cc000d5f1f423de25a77f6a6c2 Gerrit-PatchSet: 2 Gerrit-Project: mediawiki/extensions/DataValues Gerrit-Branch: master Gerrit-Owner: Daniel Werner <daniel.wer...@wikimedia.de> Gerrit-Reviewer: Tobias Gritschacher <tobias.gritschac...@wikimedia.de> Gerrit-Reviewer: jenkins-bot _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits