UNPKG

tgi-store-mongodb

Version:
1,077 lines (1,044 loc) 189 kB
/**--------------------------------------------------------------------------------------------------------------------- * tgi-core/lib/_packaging/spec-header **/ (function () { "use strict"; var root = this; var testSpec = function(spec,TGI) { var tgiCore = TGI.CORE(); var Application = tgiCore.Application; var Attribute = tgiCore.Attribute; var Command = tgiCore.Command; var Delta = tgiCore.Delta; var Interface = tgiCore.Interface; var List = tgiCore.List; var Log = tgiCore.Log; var MemoryStore = tgiCore.MemoryStore; var Message = tgiCore.Message; var Model = tgiCore.Model; var Presentation = tgiCore.Presentation; var Procedure = tgiCore.Procedure; var REPLInterface = tgiCore.REPLInterface; var Request = tgiCore.Request; var Session = tgiCore.Session; var Store = tgiCore.Store; var Text = tgiCore.Text; var Transport = tgiCore.Transport; var User = tgiCore.User; var View = tgiCore.View; var Workspace = tgiCore.Workspace; var inheritPrototype = tgiCore.inheritPrototype; var getInvalidProperties = tgiCore.getInvalidProperties; var getConstructorFromModelType = tgiCore.getConstructorFromModelType; var createModelFromModelType = tgiCore.createModelFromModelType; var trim = tgiCore.trim; var ltrim = tgiCore.ltrim; var rtrim = tgiCore.rtrim; var left = tgiCore.left; var center = tgiCore.center; var right = tgiCore.right; var lpad = tgiCore.lpad; var rpad = tgiCore.rpad; var cpad = tgiCore.cpad; var contains = tgiCore.contains; /**--------------------------------------------------------------------------------------------------------------------- * tgi-store-mongodb/lib/_packaging/spec-header **/ var MongoStore = TGI.STORE.MONGODB().MongoStore; spec.mute(true); /**--------------------------------------------------------------------------------------------------------------------- * tgi-core/lib/tgi-core.spec.js **/ spec.test('lib/tgi-spec-intro', 'tgi-core', 'Core Repository', function (callback) { spec.paragraph('Core constructors, models, stores and interfaces. The constructor functions define the object ' + '"classes" used by the framework. The Model Constructor is a key part of the core that defines the system ' + 'functionality for the framework. The framework is further extended with a Store and Interface abstract that ' + 'provides data store and user interface vendor implementations.'); spec.example('TGI.CORE function exposes library', undefined, function () { this.log(TGI.CORE().version); }); spec.index('##Constructors'); }); /**--------------------------------------------------------------------------------------------------------------------- * tgi-core/lib/tgi-core-attribute.spec.js */ spec.test('tgi-core/lib/tgi-core-attribute.spec.js', 'Attribute', 'defines data types - needed by Model', function (callback) { spec.paragraph('Attributes are the means for models to represent data of different types. They have no' + ' dependencies on Models however and can be used without creating a model.'); spec.heading('CONSTRUCTOR', function () { spec.example('objects created should be an instance of Attribute', true, function () { return new Attribute({name: 'name'}) instanceof Attribute; }); spec.example('should make sure new operator used', Error('new operator required'), function () { Attribute({name: 'name'}); // jshint ignore:line }); spec.example('should make sure properties are valid', Error('error creating Attribute: invalid property: sex'), function () { new Attribute({name: 'name', sex: 'female'}); }); spec.example('should validate and throw errors before returning from constructor', Error('error creating Attribute: multiple errors'), function () { new Attribute({eman: 'the'}); // 2 errors: name missing and eman an unknown property }); spec.heading('Attribute.ModelID', function () { spec.paragraph('Attribute.ModelID is a constructor that is used as a special type for references to IDs in external models.' + ' Note it is a function embedded as a member of the Attribute to encapsulate it.'); spec.example('objects created should be an instance of Attribute.ModelID', true, function () { return new Attribute.ModelID(new Model()) instanceof Attribute.ModelID; }); spec.example('should make sure new operator used', Error('new operator required'), function () { Attribute.ModelID(); }); spec.example('constructor must pass instance of model', Error('must be constructed with Model'), function () { new Attribute.ModelID(); }); spec.example('value is set to value of ID in constructor', 123, function () { var model = new Model(); model.set('id', 123); return new Attribute.ModelID(model).value; }); spec.example('constructorFunction is set to constructor of model', true, function () { var model = new Model(); model.set('id', 123); var attrib = new Attribute.ModelID(model); var newModel = new attrib.constructorFunction(); return newModel instanceof Model; }); spec.example('modelType is set from model in constructor', 'Model', function () { return new Attribute.ModelID(new Model()).modelType; }); spec.example('toString is more descriptive', "Model 123", function () { var model = new Model(); model.set('id', 123); return new Attribute.ModelID(model).toString(); }); spec.example('model short name used in string description if applies', 'User Error', function () { var user = new User(); user.set('name','Error'); return new Attribute.ModelID(user).toString(); }); }); }); spec.heading('PROPERTIES', function () { spec.heading('name', function () { spec.example('should be required', Error('error creating Attribute: name required'), function () { new Attribute(); }); spec.example('should allow shorthand string constructor for name property', 'Attribute: favoriteActorName = null', function () { return new Attribute('favoriteActorName'); }); }); spec.heading('type', function () { spec.example("should default to 'String'", 'String', function () { return new Attribute({name: 'name'}).type; }); spec.example('should be a valid attribute type', Error('error creating Attribute: Invalid type: Dude'), function () { this.log(Attribute.getTypes()); new Attribute({name: 'Bogus', type: "Dude"}); }); spec.example('should allow shorthand string constructor for type property', 'Date', function () { return new Attribute('favoriteActorBorn', 'Date').type; }); }); spec.heading('label', function () { spec.example('should default to name property capitalized', 'Name', function () { return new Attribute({name: 'name'}).label; }); spec.example('should be optional in constructor', 'Name', function () { return new Attribute({name: 'name', label: 'Name'}).label; }); }); spec.heading('placeHolder', function () { spec.example('pass through to Interface used as visual cue to user for input', '###-##-####', function () { return new Attribute({name: 'ssn', placeHolder: '###-##-####'}).placeHolder; }); }); spec.heading('hint', function () { spec.paragraph('hint properties give guidance in handling of the attribute'); spec.example('initialized to empty object', 'object', function () { return typeof new Attribute({name: 'name', label: 'Name'}).hint; }); }); spec.heading('hidden', function () { spec.paragraph('Attribute hidden by default'); spec.example('hidden set in constructor', undefined, function () { new Attribute({name: 'name', label: 'Name', hidden: '*'}); }); }); spec.heading('quickPick', function () { spec.example('list of values to pick from typically invoked from dropdown', 3, function () { return new Attribute({name: 'stooge', quickPick: ['moe', 'larry', 'curly']}).quickPick.length; }); }); spec.heading('validationErrors', function () { spec.example('Array of errors', undefined, function () { this.shouldBeTrue(new Attribute({name: 'name'}).validationErrors instanceof Array); this.shouldBeTrue(new Attribute({name: 'name'}).validationErrors.length === 0); }); }); spec.heading('validationMessage', function () { spec.example('string description of error(s) is empty string (falsy) when no errors', '""', function () { var errs = new Attribute({name: 'name'}).validationMessage; this.shouldBeFalsy(errs); return cpad(errs, 2, '"'); }); }); spec.heading('validationRule', function () { spec.paragraph('The validationRule property provides validation rules for attribute.' + ' For additional validation see the *Validate* event in onEvent method.'); spec.example('initialized to empty object', 'object', function () { return typeof new Attribute({name: 'name'}).validationRule; }); spec.example('can be passed to constructor', undefined, function () { new Attribute({name: 'name', validationRule: {}}); }); spec.example('validation rule is validated', Error('error creating Attribute: invalid validationRule: age'), function () { new Attribute({name: 'name', validationRule: {age: 18, required: true}}); }); spec.heading('validationRule.required', function () { spec.paragraph('validationRule.required is used when a value is required for attribute'); spec.example('validationRule.required', spec.asyncResults('Name required'), function (callback) { var a = new Attribute({name: 'name', validationRule: {required: true}}); a.validate(function () { callback(a.validationErrors); }); }); spec.example('validationRule.required for Number allows 0', spec.asyncResults(0), function (callback) { var a = new Attribute({name: 'balance', type: 'Number', value: 0, validationRule: {required: true}}); a.validate(function () { callback(a.validationErrors.length); }); }); spec.example('validationRule.required for Boolean allows false', spec.asyncResults(0), function (callback) { var a = new Attribute({name: 'active', type: 'Boolean', value: false, validationRule: {required: true}}); a.validate(function () { callback(a.validationErrors.length); }); }); }); spec.heading('validationRule.range', function () { spec.paragraph('validationRule.range is used when value must fall within a range of values- use null to omit bound'); spec.example('validationRule.range lower bound only', spec.asyncResults('Age must be at least 18'), function (callback) { var a = new Attribute({name: 'age', type: 'Number', value: 17, validationRule: {range: [18, null]}}); a.validate(function () { callback(a.validationErrors[0]); }); }); spec.example('validationRule.range upper bound only', spec.asyncResults('Age must be no more than 65'), function (callback) { var a = new Attribute({name: 'age', type: 'Number', value: 77, validationRule: {range: [null, 65]}}); a.validate(function () { callback(a.validationErrors[0]); }); }); spec.example('validationRule.range pass', spec.asyncResults(0), function (callback) { var a = new Attribute({name: 'age', type: 'Number', value: 53, validationRule: {range: [18, 65]}}); a.validate(function () { callback(a.validationErrors.length); }); }); spec.example('validationRule.range forced to array', spec.asyncResults('Age must be at least 100'), function (callback) { var a = new Attribute({name: 'age', type: 'Number', value: 53, validationRule: {range: 100}}); a.validate(function () { callback(a.validationErrors); }); }); }); spec.heading('validationRule.isOneOf', function () { spec.paragraph('validationRule.isOneOf is used when a value is must be on of items in array'); spec.example('validationRule.isOneOf fail', spec.asyncResults('Age invalid'), function (callback) { var a = new Attribute({name: 'age', type: 'Number', value: 2, validationRule: {isOneOf: [1, 3]}}); a.validate(function () { callback(a.validationErrors[0]); }); }); spec.example('validationRule.isOneOf pass', spec.asyncResults(0), function (callback) { var a = new Attribute({name: 'age', type: 'Number', value: 1, validationRule: {isOneOf: [1, 3]}}); a.validate(function () { callback(a.validationErrors.length); }); }); }); }); spec.heading('value', function () { spec.example('should accept null assignment', undefined, function () { var myTypes = Attribute.getTypes(); var record = ''; for (var i = 0; i < myTypes.length; i++) { record += myTypes[i] + ':' + new Attribute({name: 'my' + myTypes[i]}).value + ' '; } this.log(record); // It's the default and it passes constructor validation }); spec.example('should accept assignment of correct type and validate incorrect attributeTypes', '7 correct assignments 91 errors thrown', function () { // Test all known attribute types var myTypes = Attribute.getTypes(); myTypes.shift(); // not testing ID myTypes.pop(); // not testing Object since it matches other types this.log(myTypes); // Now create an array of matching values for each type into myValues var myModel = new Model(); var myGroup = new Attribute({name: 'columns', type: 'Group', value: [new Attribute("Name")]}); var myTable = new Attribute({name: 'bills', type: 'Table', group: myGroup}); var myValues = ['Jane Doe', new Date(), true, 18, new Attribute.ModelID(myModel), [], myTable]; // Loop thru each type var theGood = 0; var theBad = 0; for (var i = 0; i < myTypes.length; i++) for (var j = 0; j < myValues.length; j++) { // for the value that works it won't throw error just create and to test if (i === j) { theGood++; switch (myTypes[i]) { case 'Table': new Attribute({name: 'my' + myTypes[i], type: myTypes[i], value: myValues[j], group: myGroup}); break; default: new Attribute({name: 'my' + myTypes[i], type: myTypes[i], value: myValues[j]}); break; } } else { // mismatches bad so should throw error (is caught unless no error or different error) theBad++; // NOTE: functions in loops are terrible code practices - except in dorky test cases this.shouldThrowError('*', function () { // jshint ignore:line new Attribute({name: 'my' + myTypes[i], type: myTypes[i], value: myValues[j]}); });// jshint ignore:line } // other objects should throw always theBad++; this.shouldThrowError('*', function () { // jshint ignore:line new Attribute({name: 'my' + myTypes[i], type: myTypes[i], value: {}}); }); // jshint ignore:line } return theGood + ' correct assignments ' + theBad + ' errors thrown'; }); }); spec.heading('model', function () { spec.xexample('a reference to model', undefined, function () { return new Attribute({name: 'derp'}).model; // undefined when not part of model }); spec.xexample('defined by model', 'I am a Model', function () { var attrib = new Attribute("Sue"); new Model({attributes: [attrib]}); return 'I am ' + attrib.model; }); spec.xexample('user example', 'You are a User', function () { return 'You are ' + new User().attribute('name').model; }); }); }); spec.heading('TYPES', function () { spec.heading('ID', function () { spec.example("should have type of 'ID'", 'ID', function () { return new Attribute({name: 'CustomerID', type: 'ID'}).type; }); }); spec.heading('String', function () { spec.example("should have type of 'String'", 'String', function () { return new Attribute({name: 'Cheese', type: 'String'}).type; }); spec.example('should have size property', 10, function () { // Note: size property is not "enforced" but for formatting purposes return new Attribute({name: 'zipCode', size: 10}).size; }); spec.example('size should default to 50', 50, function () { return new Attribute({name: 'stuff'}).size; }); spec.example('size should be an integer', Error('error creating Attribute: size must be a number from 1 to 255'), function () { new Attribute({name: 'zipCode', size: "10"}); }); spec.example('size should be between 1 and 255', undefined, function () { this.shouldThrowError(Error('error creating Attribute: size must be a number from 1 to 255'), function () { new Attribute({name: 'partyLikeIts', size: 1999}); }); this.shouldThrowError(Error('error creating Attribute: size must be a number from 1 to 255'), function () { new Attribute({name: 'iGotNothing', size: 0}); }); }); spec.example('size should accept format shorthand with parens', 255, function () { return new Attribute({name: 'comments', type: 'String(255)'}).size; }); }); spec.heading('Number', function () { spec.example("type should be 'Number'", 'Number', function () { return new Attribute({name: 'healthPoints', type: 'Number'}).type; }); }); spec.heading('Date', function () { spec.example("type should be 'Date'", 'Date', function () { return new Attribute({name: 'born', type: 'Date'}).type; }); }); spec.heading('Boolean', function () { spec.example("type should be 'Boolean'", 'Boolean', function () { return new Attribute({name: 'bored', type: 'Boolean'}).type; }); }); spec.heading('Model', function () { spec.paragraph('Parameter type Model is used to store a reference to another model instance. ' + 'The value attribute is a Attribute.ModelID reference to the Model.'); spec.example('must construct with Attribute.ModelID in value', Error('error creating Attribute: value must be Attribute.ModelID'), function () { new Attribute({name: 'Twiggy', type: 'Model'}); }); spec.example("modelType property set from constructor", 'Model', function () { return new Attribute( { name: 'Twiggy', type: 'Model', value: new Attribute.ModelID(new Model()) }).modelType; }); spec.example('set method usage checks for valid type', Error('set error: value must be Attribute.ModelID'), function () { new Attribute( { name: 'Twiggy', type: 'Model', value: new Attribute.ModelID(new Model()) }).set(1); }); }); spec.heading('Group', function () { spec.paragraph('Groups are used to keep attributes together for presentation purposes.'); spec.example("should have type of 'Group'", 'Group', function () { return new Attribute({name: 'stuff', type: 'Group'}).type; }); spec.example('deep check value for valid Attributes that pass getObjectStateErrors() test', 1, function () { // this example is just to conceptualize nested components var myStuff = new Attribute("stuff", "Group"); var myCars = new Attribute("cars", "Group"); var myFood = new Attribute("food", "Group"); var myFruit = new Attribute("fruit", "Group"); var myVegs = new Attribute("vegetables", "Group"); var badApple = new Attribute('Apple'); myCars.value = [new Attribute('Nova'), new Attribute('Pinto')]; myFruit.value = [badApple, new Attribute('Peach')]; myVegs.value = [new Attribute('Carrot'), new Attribute('Beet')]; myFood.value = [myFruit, myVegs]; myStuff.value = [myFood, myCars, new Attribute('House'), new Attribute('Health')]; this.log(myStuff.getObjectStateErrors()); badApple.value = -1; // One bad apple will spoil my stuff this.log(myStuff.getObjectStateErrors()); return myStuff.getObjectStateErrors().length; }); }); spec.heading('Table', function () { spec.paragraph("Table types are used to store an array of values (rows) each of which is an array of " + "values (columns). Each column value is associated with the corresponding element in the Table " + "property group which is set when creating a Table." ); spec.example("should have type of 'Table'", 'Table', function () { var name = new Attribute("Name"); var cols = new Attribute({name: 'columns', type: 'Group', value: [name]}); return new Attribute({name: 'bills', type: 'Table', group: cols}).type; }); spec.example("group property must be defined", Error('error creating Attribute: group property required'), function () { new Attribute({name: 'details', type: 'Table'}); }); spec.example("group property must not be empty array", Error('error creating Attribute: group property value must contain at least one Attribute'), function () { var cols = new Attribute({name: 'columns', type: 'Group', value: []}); new Attribute({name: 'details', type: 'Table', group: cols}); }); }); spec.heading('Object', function () { spec.paragraph('Javascript objects ... structure user defined'); spec.example("should have type of 'Object'", 'Object', function () { return new Attribute({name: 'stuff', type: 'Object'}).type; }); }); }); spec.heading('METHODS', function () { spec.heading('get()', function () { spec.example('return value', 'John', function () { return new Attribute({name: 'name', value: 'John'}).get(); }); }); spec.heading('set()', function () { spec.example('set value', 'Jack', function () { var dude = new Attribute({name: 'name', value: 'John'}); dude.set('Jack'); return dude.get(); }); }); spec.heading('toString()', function () { spec.example('should return a description of the attribute', 'Attribute: name = null', function () { return new Attribute({name: 'name'}).toString(); }); }); spec.heading('coerce(newValue)', function () { spec.paragraph('Method returns the type equivalent of newValue for the owner objects type.'); spec.example('coerce method basic usage', undefined, function () { var myString = new Attribute({name: 'name', size: 10}); var myNumber = new Attribute({name: 'age', type: 'Number'}); var myBool = new Attribute({name: 'active', type: 'Boolean'}); var myDate = new Attribute({name: 'date', type: 'Date'}); var myGroup = new Attribute({name: 'columns', type: 'Group', value: [new Attribute("Name")]}); var myTable = new Attribute({name: 'bills', type: 'Table', group: myGroup}); // Strings this.shouldBeTrue(myString.coerce() === ''); this.shouldBeTrue(myString.coerce(false) === 'false'); this.shouldBeTrue(myString.coerce(12) === '12'); this.shouldBeTrue(myString.coerce(1 / 0) === 'Infinity'); this.shouldBeTrue(myString.coerce('01234567890') === '0123456789'); this.shouldBeTrue(myString.coerce() === ''); // Numbers this.shouldBeTrue(myNumber.coerce() === 0); this.shouldBeTrue(myNumber.coerce(false) === 0); this.shouldBeTrue(myNumber.coerce(true) === 1); this.shouldBeTrue(myNumber.coerce(' 007 ') === 7); this.shouldBeTrue(myNumber.coerce(' $123,456.78 ') === 123456.78); this.shouldBeTrue(myNumber.coerce(' $123, 456.78 ') === 123); // space will split this.shouldBeTrue(myNumber.coerce('4/20') === 0); // slash kills it // Boolean this.shouldBeTrue(myBool.coerce() === false && myBool.coerce(null) === false && myBool.coerce(0) === false); this.shouldBeTrue(myBool.coerce(true) === true && myBool.coerce(1) === true); this.shouldBeTrue(myBool.coerce('y') && myBool.coerce('yEs') && myBool.coerce('t') && myBool.coerce('TRUE') && myBool.coerce('1')); this.shouldBeTrue(!((myBool.coerce('') || (myBool.coerce('yep'))))); //// Date {todo this will break in 2018} this.shouldBeTrue(myDate.coerce('2/21/2017').getTime() === new Date('2/21/2017').getTime()); this.shouldBeTrue(myDate.coerce('2/21').getTime() === new Date('2/21/2017').getTime()); // TODO this.shouldThrowError(Error('coerce cannot determine appropriate value'), function () { new Attribute({name: 'Twiggy', type: 'Model', value: new Attribute.ModelID(new Model())}).coerce(); }); this.shouldThrowError(Error('coerce cannot determine appropriate value'), function () { new Attribute(myGroup.coerce()); }); this.shouldThrowError(Error('coerce cannot determine appropriate value'), function () { new Attribute(myTable.coerce()); }); }); }); spec.heading('getObjectStateErrors', function () { spec.example('should return array of validation errors', undefined, function () { this.shouldBeTrue(new Attribute({name: 'name'}).getObjectStateErrors() instanceof Array); var nameHosed = new Attribute({name: 'name'}); // No errors this.shouldBeTrue(nameHosed.getObjectStateErrors().length === 0); nameHosed.name = ''; // 1 err this.shouldBeTrue(nameHosed.getObjectStateErrors().length === 1); nameHosed.type = ''; // 2 errors this.shouldBeTrue(nameHosed.getObjectStateErrors().length === 2); }); }); spec.heading('onEvent', function () { spec.paragraph('Use onEvent(events,callback)'); spec.example('first parameter is a string or array of event subscriptions', Error('subscription string or array required'), function () { new Attribute({name: 'name'}).onEvent(); }); spec.example('callback is required', Error('callback is required'), function () { new Attribute({name: 'name'}).onEvent([]); }); spec.example('events are checked against known types', Error('Unknown command event: onDrunk'), function () { new Attribute({name: 'name'}).onEvent(['onDrunk'], function () { }); }); spec.example('here is a working version', undefined, function () { this.log(Attribute.getEvents()); // Validate - callback when attribute needs to be validated // StateChange -- callback when state of object (value or validation state) has changed new Attribute({name: 'name'}).onEvent(['Validate'], function () { }); }); }); spec.heading('offEvents', function () { spec.paragraph('Free all onEvent listeners'); spec.example('example', undefined, function () { new Attribute({name: 'name'}).offEvent(); }); }); spec.heading('validate', function () { spec.paragraph('check valid object state and value for attribute - invoke callback for results'); spec.example('callback is required', Error('callback is required'), function () { new Attribute({name: 'name'}).validate(); }); }); spec.heading('setError', function () { spec.paragraph('Set a error condition and descriptive message'); spec.example('first argument condition required', Error('condition required'), function () { new Attribute({name: 'status'}).setError(); }); spec.example('second argument description required', Error('description required'), function () { new Attribute({name: 'status'}).setError('login'); }); }); spec.heading('clearError', function () { spec.paragraph('Clear a error condition'); spec.example('first argument condition required', Error('condition required'), function () { new Attribute({name: 'status'}).clearError(); }); }); spec.heading('Attribute.getTypes', function () { spec.paragraph('This helper function returns an array of valid Attribute types. This is just a function - not a prototype method.'); spec.example('show the types', undefined, function () { this.log(Attribute.getTypes()); }); }); spec.heading('Attribute.getEvents', function () { spec.paragraph('This helper function returns an array of valid Attribute events. This is just a function - not a prototype method.'); spec.example('show the events', undefined, function () { this.log(Attribute.getEvents()); }); }); }); spec.heading('INTEGRATION', function () { spec.example('validation usage demonstrated', spec.asyncResults('got milk'), function (callback) { var thisCrap = this; var attribute = new Attribute({name: 'test'}); // Monitor state changes attribute.onEvent('StateChange', function () { // When the error is got milk then test is done if (attribute.validationMessage === 'got milk') callback('got milk'); }); // validate will first make sure the object passes integrity checks attribute.name = ''; attribute.validate(test1); // verify error seen function test1() { thisCrap.shouldBeTrue(attribute.validationMessage == 'name required'); // Create a validation rule - value must be equal to 42 attribute.onEvent('Validate', function () { if (attribute.value !== 42) attribute.validationErrors.push('Incorrect answer'); }); attribute.validate(test2); } // same error in message function test2() { thisCrap.shouldBeTrue(attribute.validationMessage == 'name required'); attribute.name = 'answer'; attribute.validate(test3); } // Now validation function error is shown function test3() { thisCrap.shouldBeTrue(attribute.validationMessage == 'Incorrect answer'); // Fix everything attribute.value = 42; attribute.validate(test4); } // Type is wrong function test4() { thisCrap.shouldBeTrue(attribute.validationMessage == 'value must be null or a String'); // Fix type attribute.type = 'Number'; attribute.validate(test5); } // Should have no errors function test5() { thisCrap.shouldBeTrue(attribute.validationMessage === ''); attribute.setError('uno', 'uno failed'); attribute.setError('milk', 'and cookies'); attribute.setError('milk', 'got milk'); // test overwrite of same condition diff msg attribute.validate(test6); } // now error is first set error function test6() { thisCrap.shouldBeTrue(attribute.validationMessage == 'uno failed'); attribute.clearError('zzz'); // delete a prop that does not exists is silent attribute.clearError('uno'); attribute.validate(function () { // }); } }); }); }); /**--------------------------------------------------------------------------------------------------------------------- * tgi-core/lib/tgi-core-command.spec.js */ spec.test('tgi-core/lib/tgi-core-command.spec.js', 'Command', 'encapsulates task execution', function (callback) { spec.paragraph('Command is an abstraction for task execution. It provides multiple methods of task execution ' + 'and manages the overall state of both synchronous and asynchronous processes. ' + 'The primary use is to have a simple API method to respond to UI tasks. ' + 'It can be used for processing / validation / storage / reporting type of use cases since ' + 'it handles the asynchronous nature of javascript and abstracts in a way to easily incorporate application logic.'); spec.heading('CONSTRUCTOR', function () { spec.example('objects created should be an instance of Command', true, function () { return new Command({name: 'about'}) instanceof Command; }); spec.example('should make sure new operator used', Error('new operator required'), function () { Command({name: 'about'}); // jshint ignore:line }); spec.example('should make sure argument properties are valid', Error('error creating Command: invalid property: sex'), function () { new Command({name: 'name', sex: 'female'}); }); spec.example('defaults name to a command', 'a command', function () { return new Command().name; }); spec.example('defaults type to Stub', 'Stub', function () { return new Command({name: 'about'}).type; }); }); spec.heading('PROPERTIES', function () { spec.heading('name', function () { spec.example('identifier name for command', 'about', function () { this.shouldThrowError(Error('name must be string'), function () { new Command({name: 42}); }); return new Command({name: 'about'}).name; }); }); spec.heading('description', function () { spec.example('more descriptive than name', 'Tequila Command : Tequila is a beverage made from blue agave.', function () { // description set to (name) Command if not specified return new Command({name: 'Tequila'}).description + ' : ' + new Command({name: 'tequila', description: 'Tequila is a beverage made from blue agave.'}).description; }); }); spec.heading('type', function () { spec.example('type of command must be valid', Error('Invalid command type: magic'), function () { this.log(Command.getTypes()); new Command({name: 'about', type: 'magic'}); }); }); spec.heading('contents', function () { spec.paragraph('Contents is based on the type of command. See TYPE section for more information for how it ' + 'applies to each type'); }); spec.heading('scope', function () { spec.paragraph('Optional scope property can be used to apply a model or list to a command.'); spec.example('scope must be a Model or a List', Error('optional scope property must be Model or List'), function () { new Command({name: 'archiveData', scope: true}); }); }); spec.heading('status', function () { spec.paragraph('The status property is a Number defined as negative(FAIL) positive(SUCCESS) zero(executing) ' + 'null/undefined(not executing).'); spec.paragraph('Applications can give meaning to numeric values (lt -1 and gt 1) as long as sign is retained.'); }); spec.heading('timeout', function () { spec.paragraph('Will use library setting as default, override to set the default timeout for steps used in ' + 'procedures. Value is milliseconds (1000 = 1 second)'); spec.example('number required', Error('timeout must be a Number'), function () { new Command({name: 'options', timeout: true}); }); }); spec.heading('theme', function () { spec.paragraph('Valid themes listed in example. These were inspired by bootstrap so fit that well and is' + ' optional for implementation but should follow if possible. Example PDF is b/w and may ignore ot more likely' + ' will never apply.'); spec.example('theme attribute provides visual cue', undefined, function () { // The good new Command({name: 'options', theme: 'default'}); new Command({name: 'options', theme: 'primary'}); new Command({name: 'options', theme: 'success'}); new Command({name: 'options', theme: 'info'}); new Command({name: 'options', theme: 'warning'}); new Command({name: 'options', theme: 'danger'}); new Command({name: 'options', theme: 'link'}); // The bad this.shouldThrowError(Error('invalid theme'), function () { new Command({name: 'options', theme: 'Silly'}); }); // The ugly this.shouldThrowError(Error('invalid theme'), function () { new Command({name: 'options', theme: true}); }); }); }); spec.heading('icon', function () { spec.paragraph('The icon attribute gives a graphical association to the command.' + ' They are interface specific and do break the abstractness of this library but can be ignored by' + ' other interfaces safely.'); spec.example('must be string and have prefix for 2 supported icon sets ' + 'http://glyphicons.com/ http://fontawesome.io/', undefined, function () { this.shouldThrowError(Error('invalid icon'), function () { new Command({name: 'options', icon: true}); }); this.shouldThrowError(Error('invalid icon'), function () { new Command({name: 'options', icon: 'wtf-lol'}); }); // Only prefix is validated new Command({name: 'options', icon: 'fa-whatever'}); new Command({name: 'options', icon: 'glyphicon-who-cares'}); // Must have something to the right of the dash this.shouldThrowError(Error('invalid icon'), function () { new Command({name: 'options', icon: 'fa'}); }); }); }); spec.heading('location', function () { spec.example('optional for control location {x,y}', undefined, function () { new Command({name: 'options', location: {x: 0, y: 0}}); }); }); spec.heading('images', function () { spec.example('optional for control graphical representation', undefined, function () { new Command({name: 'options', images: []}); }); }); spec.heading('presentationMode', function () { spec.paragraph('this property is used for presentation commands to specify the mode of presentation'); spec.example('default is View', 'View', function () { return new Command({type: 'Presentation', contents: new Presentation()}).presentationMode; }); spec.example('can supply in constructor', 'Edit', function () { return new Command({ type: 'Presentation', contents: new Presentation(), presentationMode: 'Edit' }).presentationMode; }); spec.example('must be valid mode', Error('Invalid presentationMode: Projector'), function () { this.log(Command.getPresentationModes()); new Command({type: 'Presentation', contents: new Presentation(), presentationMode: 'Projector'}); }); }); spec.heading('bucket', function () { spec.example('valid property is for app use', 'bucket of KFC', function () { // no real test but library will never use this word in general (TODO expand somehow ... ?). return 'bucket of ' + new Command({bucket: 'KFC'}).bucket; }); }); }); spec.heading('TYPES', function () { spec.heading('menu', function () { spec.paragraph('The menu command is passed to _Interface_ for use for in user navigation. ' + 'They are embedded in the _Application_ as the primary navigate but can be instantiated and given to ' + '_Interface_ in any context.'); spec.paragraph('The _Command_ contents property is an array _Command_ objects.'); spec.example('constructor validates the contents', undefined, function () { this.shouldThrowError(Error('contents must be array of menu items'), function () { new Command({name: 'options', type: 'Menu'}); }); this.shouldThrowError(Error('contents must be array of menu items'), function () { new Command({name: 'options', type: 'Menu', contents: []}); }); this.shouldThrowError(Error('contents must be array of menu items'), function () { new Command({name: 'options', type: 'Menu', contents: [42]}); }); // This is a working example: new Command({ name: 'options', type: 'Menu', contents: [ 'Stooges', // strings act as menu titles or non selectable choices '-', // dash is menu separator new Command({name: 'Tequila'}) // use commands for actual menu items ] }); }); }); spec.heading('Presentation', function () { spec.example('for Presentation type contents is a Presentation object', undefined, function () { this.shouldThrowError(Error('contents must be a Presentation'), function () { new Command({name: 'options', type: 'Presentation'}); }); }); }); spec.heading('Function', function () { spec.paragraph('contents contains a javascript function'); spec.example('for Function type contents is a Function', undefined, function () { this.shouldThrowError(Error('contents must be a Function'), function () { new Command({name: 'options', type: 'Function'}); }); }); spec.example('shorthand version', 'Function', function () { return new Command(function () {}).type; }); }); spec.heading('Procedure', function () { spec.example('for Procedure type contents is a Procedure object', undefined, function () { this.shouldThrowError(Error('contents must be a Procedure'), function () { new Command({name: 'options', type: 'Procedure'}); }); }); spec.example('shorthand version', 'Procedure', function () { return new Command(new Procedure()).type; }); }); }); spec.heading('METHODS', function () { spec.heading('toString', function () { spec.example('returns string including name and type', 'I am a Stub Command: Customer', function () { return 'I am a ' + new Command({name: 'Customer'}); }); }); spec.heading('abort', function () { spec.paragraph('aborts task'); spec.example('aborted command ends with error status', -1, function () { var cmd = new Command(); cmd.abort(); return cmd.status; }); }); spec.heading('complete', function () { spec.paragraph('completes task'); spec.example('call when task complete status', 1, function () { var cmd = new Command(); cmd.complete(); return cmd.status; }); }); spec.heading('execute', function () { spec.paragraph('executes task'); spec.example('see integration tests for usage', Error('command type Stub not implemented'), function () { new Command().execute(); }); spec.example('presentation commands require interface param', Error('interface param required'), function () { new Command({type: 'Presentation', contents: new Presentation()}).execute(); }); }); spec.heading('restart', function () { spec.paragraph('restarts task'); spec.example('see integration tests', Error('command type Stub not implemented'), function () { new Command().restart(); }); }); spec.heading('onEvent', function () { spec.paragraph('Use onEvent(events,callback)'); spec.example('first parameter is a string or array of event subscriptions', Error('subscription string or array required'), function () { new Command().onEvent(); }); spec.example('callback is required', Error('callback is required'), function () { new Command().onEvent([]); }); spec.example('events are checked against known types', Error('Unknown command event: onDrunk'), function () { new Command().onEvent(['onDrunk'], function () { }); }); spec.example('here is a working version', undefined, function () { this.log(Command.getEvents()); // BeforeExecute - callback called before first task executed but after tasks initialized // AfterExecute - callback called after initial task(s) launched (see onCompletion) // Error - error occurred (return {errorClear:true}) // Aborted - procedure aborted - should clean up resources // Completed - execution is complete check status property new Command().onEvent(['Completed'], function () { }); }); }); spec.heading('Command.getTypes', function () { spec.paragraph('This helper function returns an array of valid Command types. This is just a function - not a prototype method.'); spec.example('show the types', undefined, function () { this.log(Command.getTypes()); }); }); spec.heading('Command.getEvents', function () { spec.paragraph('This helper function returns an array of valid Command events. This is just a function - not a prototype method.'); spec.example('show the events', undefined, function () { this.log(Command.getEvents()); }); }); }); spec.heading('INTEGRATION', function () { spec.paragraph('test each command type'); // Stub spec.example('Stub', Error('command type Stub not implemented'), function () { var cmd = new Command({ name: 'stubCommand', description: 'stub command test', type: 'Stub' }); this.log(cmd); cmd.execute(); }); // Menu todo - placeholder or not needed? spec.xexample('Menu', Error('command type Menu not implemented'), function () { var cmd = new Command({ name: 'menuCommand', description: 'menu command test', type: 'Menu', contents: ['Hello World'] }); this.log(cmd); cmd.execute(); }); // Presentation spec.example('Presentation', undefined, function () { var cmd = new Command({ name: 'presentationCommand', description: 'presentation command test', type: 'Presentation', contents: new Presentation() }); this.shouldThrowError(Error('contents must be a Presentation'), function () { cmd.contents = 123; cmd.execute(); }); this.shouldThrowError(Error('error executing Presentation: contents elements must be Text, Command, Attribute, List or string'), function () { cmd.contents = new Presentation(); cmd.contents.set('contents', [new Command(), new Attribute({name: 'meh'}), true]); cmd.execute(); }); }); // Function spec.example('Function test straight up', spec.asyncResults('Hola! BeforeExecute AfterExecute Adious! funk Completed BeforeExecute AfterExecute funk Completed'), function (callback) { var execCount = 0; // Call twice to test reset state var cmd = new Command({ type: 'Function', contents: function () { this.bucket += ' funk'; this.complete(); } }); cmd.bucket = 'Hola!'; // Monitor all events cmd.onEvent(['BeforeExecute', 'AfterExecute', 'Error', 'Aborted', 'Completed'], function (event) { this.bucket += ' ' + event; if (event == 'Completed') { if (execCount++ < 2) cmd.execute(); else callback(this.bucket); } }); execCount++; cmd.execute(); cmd.bucket += ' Adious!'; }); // Function error spec.example('Function test with error', spec.asyncResults('Hola! BeforeExecute AfterExecute Adious! funk Error Completed'), function (callback) { var cmd = new Command({ type: 'Function', contents: function () { this.bucket += ' funk'; throw new Error('function go boom!'); } }); cmd.bucket = 'Hola!'; // Monitor all events cmd.onEvent('*', function (event) { // * for all events this.bucket += ' ' + event; if (event == 'Completed') callback(this.bucket); }); cmd.execute(); cmd.bucket += ' Adious!'; }); // Function abort spec.example('Function test with abort', spec.asyncResults('Hola! BeforeExecute AfterExecute Adious! funk Aborted Completed'), function (callback) { var cmd = new Command({ type: 'Function', contents: function () { this.bucket += ' funk'; this.abort(); } }); cmd.bucket = 'Hola!'; // Monitor all events cmd.onEvent(['BeforeExecute', 'AfterExecute', 'Error', 'Aborted', 'Completed'], function (event) { this.bucket += ' ' + event; if (event == 'Completed') callback(this.bucket); }); cmd.execute(); cmd.bucket += ' Adious!'; }); // Procedure spec.example('Procedure', undefined, function () { var cmd = new Command({ name: 'procedureCommand', description: 'procedure command test', type: 'Procedure', contents: new Procedure() }); this.log(cmd); cmd.execute(); }); spec.paragraph('(Better example under `Procedure` Constructer)'); spec.paragraph('More stuff'); spec.example('Error event passes error object', spec.asyncResults('Error: boom'), function (callback) { var cmd = new Command({ type: 'Function', contents: function () { throw new Error('boom'); } }); cmd.onEvent(['Error'], fu