Enable changing Task structure

Provide unit-tests for verifying that Task structure depends both on
parent Workflow type and on Task type.

This commits also depends on a change made to Barricade.js library:
now it passes the parameters of a parent container to the children
containers, which add new elements to it via prototype inheritance.

Closes-Bug: #1446230
Implements: blueprint merlin-unittests
Change-Id: Ic4c0539297d6df9a0b1450a824eeca4749455cfd
This commit is contained in:
Timur Sufiev 2015-04-22 18:39:50 +03:00
parent 05f4f22b9e
commit 0b92f674a4
8 changed files with 448 additions and 81 deletions

View File

@ -21,7 +21,7 @@
function getWorkbookNextIDSuffix(base) {
var containerName = base + 's',
regexp = /(workflow|action)([0-9]+)/,
container = workbook.get(containerName);
container = $scope.workbook.get(containerName);
if ( !container ) {
throw 'Base should be either "action" or "workflow"!';
}
@ -33,13 +33,15 @@
$scope.addAction = function() {
var nextSuffix = getWorkbookNextIDSuffix(baseActionId),
newID = baseActionId + nextSuffix;
workbook.get('actions').push({name: 'Action ' + nextSuffix}, {id: newID});
$scope.workbook.get('actions').push(
{name: 'Action ' + nextSuffix}, {id: newID});
};
$scope.addWorkflow = function() {
var nextSuffix = getWorkbookNextIDSuffix(baseWorkflowId),
newID = baseWorkflowId + nextSuffix;
workbook.get('workflows').push({name: 'Workflow ' + nextSuffix}, {id: newID});
$scope.workbook.get('workflows').push(
{name: 'Workflow ' + nextSuffix}, {id: newID});
};
}])

View File

@ -197,6 +197,15 @@
});
models.Task = fields.frozendict.extend({
create: function(json, parameters) {
var self = fields.frozendict.create.call(this, json, parameters);
self.on('childChange', function(child, op) {
if ( child === self.get('type') && op !== 'id' ) {
self.emit('change', 'taskType');
}
});
return self;
},
_getPrettyJSON: function() {
var json = fields.frozendict._getPrettyJSON.apply(this, arguments);
delete json.name;
@ -219,32 +228,27 @@
},
'type': {
'@class': fields.string.extend({}, {
'@enum': ['Action-based', 'Workflow-based'],
'@enum': [{
value: 'action', label: 'Action-based'
}, {
value: 'workflow', label: 'Workflow-based'
}],
'@default': 'action',
'@meta': {
'index': 1,
'row': 0
}
})
},
'action': {
'@class': fields.string.extend({}, {
'description': {
'@class': fields.text.extend({}, {
'@meta': {
'index': 2,
'index': 1,
'row': 1
}
})
},
'input': {
'@class': fields.dictionary.extend({}, {
'@meta': {
'index': 3
},
'?': {
'@class': fields.string
}
})
},
'publish': {
'@class': fields.dictionary.extend({}, {
'@meta': {
'index': 4
@ -254,35 +258,12 @@
}
})
},
'on-error': {
'@class': fields.list.extend({}, {
'publish': {
'@class': fields.dictionary.extend({}, {
'@meta': {
'title': 'On error',
'index': 5
},
'*': {
'@class': fields.string
}
})
},
'on-success': {
'@class': fields.list.extend({}, {
'@meta': {
'title': 'On success',
'index': 6
},
'*': {
'@class': fields.string
}
})
},
'on-complete': {
'@class': fields.list.extend({}, {
'@meta': {
'title': 'On complete',
'index': 7
},
'*': {
'?': {
'@class': fields.string
}
})
@ -300,7 +281,7 @@
}
}, {
'@meta': {
'index': 8
'index': 9
},
'@required': false,
'wait-before': {
@ -366,6 +347,94 @@
}
});
models.ReverseWFTask = models.Task.extend({}, {
'requires': {
'@class': fields.string.extend({}, {
'@meta': {
'row': 2,
'index': 3
}
})
}
});
models.DirectWFTask = models.Task.extend({}, {
'on-error': {
'@class': fields.list.extend({}, {
'@meta': {
'title': 'On error',
'index': 6
},
'*': {
'@class': fields.string
}
})
},
'on-success': {
'@class': fields.list.extend({}, {
'@meta': {
'title': 'On success',
'index': 7
},
'*': {
'@class': fields.string
}
})
},
'on-complete': {
'@class': fields.list.extend({}, {
'@meta': {
'title': 'On complete',
'index': 8
},
'*': {
'@class': fields.string
}
})
}
});
models.ActionTaskMixin = Barricade.Blueprint.create(function() {
return this.extend({}, {
'action': {
'@class': fields.string.extend({}, {
'@meta': {
'row': 1,
'index': 2
}
})
}
});
});
models.WorkflowTaskMixin = Barricade.Blueprint.create(function() {
return this.extend({}, {
'workflow': {
'@class': fields.string.extend({}, {
'@meta': {
'row': 1,
'index': 2
}
})
}
});
});
var taskTypes = {
'direct': models.DirectWFTask,
'reverse': models.ReverseWFTask,
'action': models.ActionTaskMixin,
'workflow': models.WorkflowTaskMixin
};
function TaskFactory(json, parameters) {
var type = json.type || 'action',
baseClass = taskTypes[parameters.wfType],
mixinClass = taskTypes[type],
taskClass = mixinClass.call(baseClass);
return taskClass.create(json, parameters);
}
models.Workflow = fields.frozendict.extend({
create: function(json, parameters) {
var self = fields.frozendict.create.call(this, json, parameters);
@ -423,13 +492,29 @@
})
},
'tasks': {
'@class': fields.dictionary.extend({}, {
'@class': fields.dictionary.extend({
create: function(json, parameters) {
var self = fields.dictionary.create.call(this, json, parameters);
self.on('childChange', function(child, op) {
if ( op === 'taskType' ) {
var taskId = child.getID(),
params = child._parameters,
taskPos = self.getPosByID(taskId),
taskData = child.toJSON();
params.id = taskId;
self.set(taskPos, TaskFactory(taskData, params));
}
});
return self;
}
}, {
'@meta': {
'index': 5,
'group': true
},
'?': {
'@class': models.Task
'@class': models.Task,
'@factory': TaskFactory
}
})
}
@ -481,6 +566,7 @@
function workflowFactory(json, parameters) {
var type = json.type || 'direct';
parameters.wfType = type;
return workflowTypes[type].create(json, parameters);
}
@ -539,11 +625,11 @@
if ( op === 'workflowType' ) {
var workflowId = child.getID(),
workflowPos = self.getPosByID(workflowId),
workflowData = child.toJSON(),
newType = child.get('type').get(),
newWorkflow = workflowTypes[newType].create(
workflowData, {id: workflowId});
self.set(workflowPos, newWorkflow);
params = child._parameters,
workflowData = child.toJSON();
params.wfType = child.type;
params.id = workflowId;
self.set(workflowPos, workflowFactory(workflowData, params));
}
});
return self;

View File

@ -0,0 +1,223 @@
/* Copyright (c) 2014 Mirantis, Inc.
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
*/
describe('workbook model logic', function() {
var models, utils, workbook;
beforeEach(function() {
module('mistral');
inject(function($injector) {
models = $injector.get('mistral.workbook.models');
utils = $injector.get('merlin.utils');
});
workbook = models.Workbook.create();
});
function getWorkflow(workflowID) {
return workbook.get('workflows').getByID(workflowID);
}
describe('defines workflow structure transformations:', function() {
var workflowID = 'workflow1';
beforeEach(function() {
workbook.get('workflows').push({name: 'Workflow 1'}, {id: workflowID});
});
it("new workflow starts as a 'direct' workflow and has proper structure", function() {
var workflow = getWorkflow(workflowID);
expect(workflow.get('type').get()).toEqual('direct');
expect(workflow.instanceof(models.DirectWorkflow)).toBe(true);
});
it("after setting type to 'reverse' the workflow structure changes to the proper one", function() {
getWorkflow(workflowID).get('type').set('reverse');
expect(getWorkflow(workflowID).instanceof(models.ReverseWorkflow)).toBe(true);
});
it("changing 'reverse' type to 'direct' again causes workflow structure to properly change", function() {
getWorkflow(workflowID).get('type').set('reverse');
getWorkflow(workflowID).get('type').set('direct');
expect(getWorkflow(workflowID).instanceof(models.DirectWorkflow)).toBe(true);
});
});
describe('defines task structure transformations', function() {
var workflowID = 'workflow1',
taskID = 'task1';
function getTask(taskID) {
return getWorkflow(workflowID).get('tasks').getByID(taskID);
}
beforeEach(function() {
workbook.get('workflows').push({name: 'Workflow 1'}, {id: workflowID});
});
describe("which start with the 'direct' workflow:", function() {
beforeEach(function() {
var workflow = getWorkflow(workflowID),
params = workflow._parameters;
workflow.get('tasks').push({name: 'Task 1'}, utils.extend(params, {id: taskID}));
});
it("new task starts as an 'action'-based one and has proper structure", function() {
expect(getTask(taskID).get('type').get()).toEqual('action');
expect(getTask(taskID).instanceof(models.ActionTaskMixin)).toBe(true);
expect(getTask(taskID).instanceof(models.DirectWFTask)).toBe(true);
});
it("changing task type from 'action' to 'workflow' causes proper structure changes", function() {
getTask(taskID).get('type').set('workflow');
expect(getTask(taskID).instanceof(models.WorkflowTaskMixin)).toBe(true);
expect(getTask(taskID).instanceof(models.DirectWFTask)).toBe(true);
});
it("changing workflow type to 'reverse' causes the proper changes to its tasks", function() {
getWorkflow(workflowID).get('type').set('reverse');
expect(getTask(taskID).instanceof(models.ActionTaskMixin)).toBe(true);
expect(getTask(taskID).instanceof(models.ReverseWFTask)).toBe(true);
getTask(taskID).get('type').set('workflow');
expect(getTask(taskID).instanceof(models.WorkflowTaskMixin)).toBe(true);
expect(getTask(taskID).instanceof(models.ReverseWFTask)).toBe(true);
});
it("changing workflow type from 'reverse' to 'direct' causes the proper changes to its tasks", function() {
getWorkflow(workflowID).get('type').set('reverse');
getWorkflow(workflowID).get('type').set('direct');
expect(getTask(taskID).instanceof(models.ActionTaskMixin)).toBe(true);
expect(getTask(taskID).instanceof(models.DirectWFTask)).toBe(true);
getTask(taskID).get('type').set('workflow');
expect(getTask(taskID).instanceof(models.WorkflowTaskMixin)).toBe(true);
expect(getTask(taskID).instanceof(models.DirectWFTask)).toBe(true);
});
});
describe("which start with the 'reverse' workflow:", function() {
beforeEach(function() {
var workflow;
getWorkflow(workflowID).get('type').set('reverse');
workflow = getWorkflow(workflowID);
workflow.get('tasks').push(
{name: 'Task 1'}, utils.extend(workflow._parameters, {id: taskID}));
});
it("new task starts as an 'action'-based one and has proper structure", function() {
expect(getTask(taskID).get('type').get()).toEqual('action');
expect(getTask(taskID).instanceof(models.ActionTaskMixin)).toBe(true);
expect(getTask(taskID).instanceof(models.ReverseWFTask)).toBe(true);
});
it("changing task type from 'action' to 'workflow' causes proper structure changes", function() {
getTask(taskID).get('type').set('workflow');
expect(getTask(taskID).instanceof(models.WorkflowTaskMixin)).toBe(true);
expect(getTask(taskID).instanceof(models.ReverseWFTask)).toBe(true);
});
it("changing workflow type to 'direct' causes the proper changes to its tasks", function() {
getWorkflow(workflowID).get('type').set('direct');
expect(getTask(taskID).instanceof(models.ActionTaskMixin)).toBe(true);
expect(getTask(taskID).instanceof(models.DirectWFTask)).toBe(true);
getTask(taskID).get('type').set('workflow');
expect(getTask(taskID).instanceof(models.WorkflowTaskMixin)).toBe(true);
expect(getTask(taskID).instanceof(models.DirectWFTask)).toBe(true);
});
it("changing workflow type from 'direct' to 'reverse' causes the proper changes to its tasks", function() {
getWorkflow(workflowID).get('type').set('direct');
getWorkflow(workflowID).get('type').set('reverse');
expect(getTask(taskID).instanceof(models.ActionTaskMixin)).toBe(true);
expect(getTask(taskID).instanceof(models.ReverseWFTask)).toBe(true);
getTask(taskID).get('type').set('workflow');
expect(getTask(taskID).instanceof(models.WorkflowTaskMixin)).toBe(true);
expect(getTask(taskID).instanceof(models.ReverseWFTask)).toBe(true);
});
});
});
describe('defines top-level actions available to user:', function() {
var $scope;
beforeEach(inject(function(_$controller_) {
var $controller = _$controller_;
$scope = {};
$controller('workbookCtrl', {$scope: $scope});
$scope.workbook = workbook;
}));
describe("'Add Action' action", function() {
it('adds a new Action', function() {
$scope.addAction();
expect(workbook.get('actions').get(0)).toBeDefined();
});
it('creates action with predefined name', function() {
$scope.addAction();
expect(workbook.get('actions').get(0).get('name').get()).toBeGreaterThan('');
});
it('creates actions with different names on 2 successive calls', function() {
$scope.addAction();
$scope.addAction();
expect(workbook.get('actions').get(0).get('name').get()).not.toEqual(
workbook.get('actions').get(1).get('name').get())
});
});
describe("'Add Workflow' action", function() {
it('adds a new Workflow', function() {
$scope.addWorkflow();
expect(workbook.get('workflows').get(0)).toBeDefined();
});
it('creates workflow with predefined name', function() {
$scope.addWorkflow();
expect(workbook.get('workflows').get(0).get('name').get()).toBeGreaterThan('');
});
it('creates workflows with different names on 2 successive calls', function() {
$scope.addWorkflow();
$scope.addWorkflow();
expect(workbook.get('workflows').get(0).get('name').get()).not.toEqual(
workbook.get('workflows').get(1).get('name').get())
});
});
})
});

View File

@ -33,22 +33,27 @@ module.exports = function (config) {
],
files: [
'./bower_components/angular/angular.js',
'./bower_components/angular-mocks/angular-mocks.js',
'./merlin/static/merlin/js/lib/underscore-min.js',
'./merlin/static/merlin/js/merlin.init.js',
'./merlin/static/merlin/js/merlin.templates.js',
'./merlin/static/merlin/js/merlin.directives.js',
'./merlin/static/merlin/js/merlin.filters.js',
'./merlin/static/merlin/js/merlin.field.models.js',
'./merlin/static/merlin/js/merlin.panel.models.js',
'./merlin/static/merlin/js/merlin.utils.js',
'./merlin/static/merlin/js/lib/angular-filter.js',
'./merlin/static/merlin/js/lib/barricade.js',
'./merlin/static/merlin/js/lib/js-yaml.js',
'bower_components/angular/angular.js',
'bower_components/angular-mocks/angular-mocks.js',
'merlin/static/merlin/js/lib/underscore-min.js',
'merlin/static/merlin/js/merlin.init.js',
'merlin/static/merlin/js/merlin.templates.js',
'merlin/static/merlin/js/merlin.directives.js',
'merlin/static/merlin/js/merlin.filters.js',
'merlin/static/merlin/js/merlin.field.models.js',
'merlin/static/merlin/js/merlin.panel.models.js',
'merlin/static/merlin/js/merlin.utils.js',
'merlin/static/merlin/js/lib/angular-filter.js',
'merlin/static/merlin/js/lib/barricade.js',
'merlin/static/merlin/js/lib/js-yaml.js',
'merlin/test/js/utilsSpec.js',
'merlin/test/js/templatesSpec.js',
'merlin/test/js/filtersSpec.js'
'merlin/test/js/filtersSpec.js',
'extensions/mistral/static/mistral/js/mistral.init.js',
'extensions/mistral/static/mistral/js/mistral.workbook.models.js',
'extensions/mistral/static/mistral/js/mistral.workbook.controllers.js',
'extensions/mistral/test/js/workbookSpec.js'
],
exclude: [

View File

@ -47,11 +47,12 @@ var Barricade = (function () {
*/
create: function (f) {
return function g() {
if (!Object.prototype.hasOwnProperty.call(this, '_parents')) {
Object.defineProperty(this, '_parents', {value: []});
var result = f.apply(this, arguments) || this;
if (!Object.prototype.hasOwnProperty.call(result, '_parents')) {
Object.defineProperty(result, '_parents', {value: []});
}
this._parents.push(g);
return f.apply(this, arguments);
result._parents.push(g);
return result;
};
}
};
@ -547,7 +548,7 @@ var Barricade = (function () {
create: function (json, parameters) {
var self = this.extend({}),
schema = self._schema,
isUsed;
isUsed, id;
self._parameters = parameters = parameters || {};
@ -570,7 +571,10 @@ var Barricade = (function () {
Enumerated.call(self, schema['@enum']);
}
Identifiable.call(self, parameters.id);
if ( Object.hasOwnProperty.call(parameters, 'id') ) {
id = parameters.id;
}
Identifiable.call(self, id);
return self;
},
@ -871,10 +875,10 @@ var Barricade = (function () {
* @memberof Barricade.Arraylike
* @private
*/
_sift: function (json) {
_sift: function (json, parameters) {
return json.map(function (el) {
return this._keyClassCreate(
this._elSymbol, this._elementClass, el);
this._elSymbol, this._elementClass, el, parameters);
}, this);
},
@ -1030,11 +1034,11 @@ var Barricade = (function () {
* @memberof Barricade.ImmutableObject
* @private
*/
_sift: function (json) {
_sift: function (json, parameters) {
var self = this;
return this.getKeys().reduce(function (objOut, key) {
objOut[key] =
self._keyClassCreate(key, self._keyClasses[key], json[key]);
objOut[key] = self._keyClassCreate(
key, self._keyClasses[key], json[key], parameters);
return objOut;
}, {});
},
@ -1171,10 +1175,12 @@ var Barricade = (function () {
* @memberof Barricade.MutableObject
* @private
*/
_sift: function (json) {
_sift: function (json, parameters) {
return Object.keys(json).map(function (key) {
return this._keyClassCreate(this._elSymbol, this._elementClass,
json[key], {id: key});
var params = Object.create(parameters);
params.id = key;
return this._keyClassCreate(
this._elSymbol, this._elementClass, json[key], params);
}, this);
},

View File

@ -123,7 +123,7 @@
modelMixin.call(self, 'list');
self.add = function() {
self.push();
self.push(undefined, parameters);
};
self.getValues = function() {
return self.toArray();
@ -181,7 +181,7 @@
} else { // usually, it's either frozendict inside or string
newValue = '';
}
self.push(newValue, {id: newID});
self.push(newValue, utils.extend(self._parameters, {id: newID}));
_items[newID] = self.getByID(newID);
};
self.getValues = function() {

View File

@ -82,6 +82,16 @@
}
}
function extend(proto, extension) {
var newObj;
proto = (proto !== undefined ? proto : null);
newObj = Object.create(proto);
Object.keys(extension).forEach(function(key) {
newObj[key] = extension[key];
});
return newObj;
}
return {
getMeta: getMeta,
getNewId: getNewId,
@ -89,6 +99,7 @@
makeTitle: makeTitle,
getNextIDSuffix: getNextIDSuffix,
enhanceItemWithID: enhanceItemWithID,
extend: extend,
pop: pop
}
})

View File

@ -27,7 +27,41 @@ describe('merlin.utils', function() {
expect(array.condense()).toEqual([1, 0, 15, 7, 8]);
});
});
function extend(proto, extension) {
var newObj;
proto = (proto !== undefined ? proto : null);
newObj = Object.create(proto);
Object.keys(extension).forEach(function(key) {
newObj[key] = extension[key];
});
return newObj;
}
describe('extend function', function() {
var obj;
beforeEach(function() {
obj = {
'key1': 10,
'key2': 20
};
});
it("doesn't remove existing keys from the resulting object", function() {
var newObj = extend(obj, {'key3': 30});
expect(newObj.key1).toBe(10);
expect(newObj.key3).toBe(30);
});
it('overrides keys with the same names as the ones in extension', function() {
var newObj = extend(obj, {'key2': 40});
expect(newObj.key2).toBe(40);
});
it("doesn't touch the original object, even the keys with the same names", function() {
var newObj = extend(obj, {'key2': 40, 'key4': 50});
expect(obj.key1).toBe(10);
expect(obj.key2).toBe(20);
});
})
});