test(custom-elements): add integration tests for custom elements

Closes #352
This commit is contained in:
Ricardo Matias 2015-09-08 14:38:50 +02:00 committed by Nico Rehwaldt
parent 857454bbc1
commit 1295400fe0
9 changed files with 646 additions and 21 deletions

View File

@ -1,15 +1,18 @@
'use strict';
var is = require('../../util/ModelUtil').is;
function getLabelAttr(semantic) {
if (semantic.$instanceOf('bpmn:FlowElement') ||
semantic.$instanceOf('bpmn:Participant') ||
semantic.$instanceOf('bpmn:Lane') ||
semantic.$instanceOf('bpmn:SequenceFlow') ||
semantic.$instanceOf('bpmn:MessageFlow')) {
if (is(semantic, 'bpmn:FlowElement') ||
is(semantic, 'bpmn:Participant') ||
is(semantic, 'bpmn:Lane') ||
is(semantic, 'bpmn:SequenceFlow') ||
is(semantic, 'bpmn:MessageFlow')) {
return 'name';
}
if (semantic.$instanceOf('bpmn:TextAnnotation')) {
if (is(semantic, 'bpmn:TextAnnotation')) {
return 'text';
}
}

View File

@ -2,6 +2,7 @@
var assign = require('lodash/object/assign');
var is = require('./ModelUtil').is;
var DEFAULT_LABEL_SIZE = module.exports.DEFAULT_LABEL_SIZE = {
width: 90,
@ -16,17 +17,12 @@ var DEFAULT_LABEL_SIZE = module.exports.DEFAULT_LABEL_SIZE = {
* @return {Boolean} true if has label
*/
module.exports.hasExternalLabel = function(semantic) {
// custom elements
if (typeof semantic.$instanceOf !== 'function') {
return false;
}
return semantic.$instanceOf('bpmn:Event') ||
semantic.$instanceOf('bpmn:Gateway') ||
semantic.$instanceOf('bpmn:DataStoreReference') ||
semantic.$instanceOf('bpmn:DataObjectReference') ||
semantic.$instanceOf('bpmn:SequenceFlow') ||
semantic.$instanceOf('bpmn:MessageFlow');
return is(semantic, 'bpmn:Event') ||
is(semantic, 'bpmn:Gateway') ||
is(semantic, 'bpmn:DataStoreReference') ||
is(semantic, 'bpmn:DataObjectReference') ||
is(semantic, 'bpmn:SequenceFlow') ||
is(semantic, 'bpmn:MessageFlow');
};
@ -36,7 +32,7 @@ module.exports.hasExternalLabel = function(semantic) {
* @param {Array<Point>} waypoints
* @return {Point} the mid point
*/
var getWaypointsMid = module.exports.getWaypointsMid = function(waypoints) {
function getWaypointsMid(waypoints) {
var mid = waypoints.length / 2 - 1;
@ -47,10 +43,12 @@ var getWaypointsMid = module.exports.getWaypointsMid = function(waypoints) {
x: first.x + (second.x - first.x) / 2,
y: first.y + (second.y - first.y) / 2
};
};
}
module.exports.getWaypointsMid = getWaypointsMid;
var getExternalLabelMid = module.exports.getExternalLabelMid = function(element) {
function getExternalLabelMid(element) {
if (element.waypoints) {
return getWaypointsMid(element.waypoints);
@ -60,7 +58,10 @@ var getExternalLabelMid = module.exports.getExternalLabelMid = function(element)
y: element.y + element.height + DEFAULT_LABEL_SIZE.height / 2
};
}
};
}
module.exports.getExternalLabelMid = getExternalLabelMid;
/**
* Returns the bounds of an elements label, parsed from the elements DI or

View File

@ -192,6 +192,7 @@ function inject(fn) {
}
module.exports.bootstrapBpmnJS = (window || global).bootstrapBpmnJS = bootstrapBpmnJS;
module.exports.bootstrapModeler = (window || global).bootstrapModeler = bootstrapModeler;
module.exports.bootstrapViewer = (window || global).bootstrapViewer = bootstrapViewer;
module.exports.inject = (window || global).inject = inject;

View File

@ -0,0 +1,170 @@
'use strict';
/* global bootstrapModeler, inject */
var Modeler = require('../../lib/Modeler');
var canvasEvent = require('../util/MockEvents').createCanvasEvent;
var customElementsModules = require('./custom-elements'),
noTouchInteractionModule = { touchInteractionEvents: ['value', null ]},
modelerModules = Modeler.prototype._modules,
customModules = [ customElementsModules, noTouchInteractionModule ];
var testModules = [].concat(modelerModules, customModules);
describe('custom elements', function() {
var diagramXML = require('../fixtures/bpmn/simple.bpmn');
beforeEach(bootstrapModeler(diagramXML, {
modules: testModules
}));
describe('renderer', function () {
var triangle, circle;
beforeEach(inject(function(elementFactory, canvas) {
triangle = elementFactory.createShape({
id: 'triangle',
type: 'custom:triangle',
x: 700, y: 100
});
canvas.addShape(triangle);
circle = elementFactory.createShape({
id: 'circle',
type: 'custom:circle',
x: 800, y: 100
});
canvas.addShape(circle);
}));
it('should render custom elements', inject(function(elementRegistry) {
// when
// then
expect(elementRegistry.get('triangle')).to.eql(triangle);
expect(elementRegistry.get('circle')).to.eql(circle);
}));
it('should get the correct custom elements path', inject(function(graphicsFactory) {
// when
var trianglePath = graphicsFactory.getShapePath(triangle),
circlePath = graphicsFactory.getShapePath(circle);
// then
expect(trianglePath).to.equal('M720,100l20,40l-40,0z');
expect(circlePath).to.equal('M870,170m0,-70a70,70,0,1,1,0,140a70,70,0,1,1,0,-140z');
}));
it('should still render bpmn elements', inject(function(elementFactory) {
// when
var startEvent = elementFactory.createShape({ type: 'bpmn:StartEvent' });
// then
expect(startEvent.businessObject.$type).to.equal('bpmn:StartEvent');
}));
});
describe('integration', function () {
var triangle, circle;
beforeEach(inject(function(elementFactory, canvas) {
circle = elementFactory.createShape({
id: 'circle',
type: 'custom:circle',
x: 800, y: 100
});
canvas.addShape(circle);
triangle = elementFactory.createShape({
id: 'triangle',
type: 'custom:triangle',
x: 700, y: 100
});
canvas.addShape(triangle);
}));
it('should allow moving a custom shape inside another one',
inject(function(elementFactory, elementRegistry, dragging, move) {
// given
var circleGfx = elementRegistry.getGraphics(circle);
// when
move.start(canvasEvent({ x: 0, y: 0 }), triangle);
dragging.move(canvasEvent({ x: 100, y: 0 }));
dragging.hover({ element: circle, gfx: circleGfx });
dragging.move(canvasEvent({ x: 150, y: 50 }));
dragging.end();
// then
expect(triangle.parent).to.equal(circle);
}));
it('should update the custom shape properties',
inject(function(elementFactory, elementRegistry, dragging, move) {
// given
var circleGfx = elementRegistry.getGraphics(circle);
// when
move.start(canvasEvent({ x: 0, y: 0 }), triangle);
dragging.move(canvasEvent({ x: 100, y: 0 }));
dragging.hover({ element: circle, gfx: circleGfx });
dragging.move(canvasEvent({ x: 150, y: 50 }));
dragging.end();
// then
expect(triangle.businessObject.leader).to.equal(circle);
expect(circle.businessObject.companions).to.include(triangle);
}));
it('should connect a bpmn element to a custom one',
inject(function(elementFactory, dragging, elementRegistry, connect) {
// given
var subProcess = elementRegistry.get('SubProcess_1'),
triangleGfx = elementRegistry.getGraphics(triangle);
// when
connect.start(canvasEvent({ x: 590, y: 90 }), subProcess);
dragging.move(canvasEvent({ x: 700, y: 100 }));
dragging.hover({ element: triangle, gfx: triangleGfx });
dragging.move(canvasEvent({ x: 715, y: 115 }));
dragging.end();
var connection = triangle.incoming[0];
// then
expect(connection.type).to.equal('bpmn:Association');
expect(connection.source).to.equal(subProcess);
}));
});
});

View File

@ -0,0 +1,77 @@
'use strict';
var assign = require('lodash/object/assign'),
inherits = require('inherits');
var BpmnElementFactory = require('../../../lib/features/modeling/ElementFactory'),
LabelUtil = require('../../../lib/util/LabelUtil');
function CustomElementFactory(bpmnFactory, moddle) {
BpmnElementFactory.call(this, bpmnFactory, moddle);
var self = this;
this.create = function(elementType, attrs) {
var type = attrs.type,
businessObject,
size;
if (elementType === 'label') {
return self.baseCreate(elementType, assign({ type: 'label' }, LabelUtil.DEFAULT_LABEL_SIZE, attrs));
}
if (/^custom\:/.test(type)) {
type = attrs.type.replace(/^custom\:/, '');
businessObject = {};
size = self._getCustomElementSize(type);
return self.baseCreate(elementType,
assign({ type: elementType, businessObject: businessObject }, attrs, size));
}
return self.createBpmnElement(elementType, attrs);
};
}
inherits(CustomElementFactory, BpmnElementFactory);
module.exports = CustomElementFactory;
CustomElementFactory.$inject = [ 'bpmnFactory', 'moddle' ];
/**
* Sets the *width* and *height* for custom shapes.
*
* The following example shows an interface on how
* to setup the custom element's dimensions.
*
* @example
*
* var shapes = {
* triangle: { width: 40, height: 40 },
* rectangle: { width: 100, height: 20 }
* };
*
* return shapes[type];
*
*
* @param {String} type
*
* @return {Bounds} { width, height}
*/
CustomElementFactory.prototype._getCustomElementSize = function (type) {
if (!type) {
return { width: 100, height: 80 };
}
var shapes = {
triangle: { width: 40, height: 40 },
circle: { width: 140, height: 140 }
};
return shapes[type];
};

View File

@ -0,0 +1,127 @@
'use strict';
var inherits = require('inherits');
var BaseRenderer = require('diagram-js/lib/draw/BaseRenderer');
var componentsToPath = require('diagram-js/lib/util/RenderUtil').componentsToPath;
function CustomRenderer(eventBus, styles) {
BaseRenderer.call(this, eventBus, 2000);
this._styles = styles;
var self = this;
var computeStyle = styles.computeStyle;
this.handlers = {
'custom:triangle': function(p, element) {
return self.drawTriangle(p, element.width);
},
'custom:circle': function(p, element, attrs) {
return self.drawCircle(p, element.width, element.height, attrs);
}
};
this.drawTriangle = function(p, side, attrs) {
var halfSide = side / 2,
points;
points = [ halfSide, 0, side, side, 0, side ];
attrs = computeStyle(attrs, {
stroke: '#3CAA82',
strokeWidth: 2,
fill: '#3CAA82'
});
return p.polygon(points).attr(attrs);
};
this.getTrianglePath = function(element) {
var x = element.x,
y = element.y,
width = element.width,
height = element.height;
var trianglePath = [
['M', x + width / 2, y],
['l', width / 2, height],
['l', -width, 0 ],
['z']
];
return componentsToPath(trianglePath);
};
this.drawCircle = function(p, width, height, attrs) {
var cx = width / 2,
cy = height / 2;
attrs = computeStyle(attrs, {
stroke: '#4488aa',
strokeWidth: 4,
fill: 'white'
});
return p.circle(cx, cy, Math.round((width + height) / 4)).attr(attrs);
};
this.getCirclePath = function(shape) {
var cx = shape.x + shape.width / 2,
cy = shape.y + shape.height / 2,
radius = shape.width / 2;
var circlePath = [
['M', cx, cy],
['m', 0, -radius],
['a', radius, radius, 0, 1, 1, 0, 2 * radius],
['a', radius, radius, 0, 1, 1, 0, -2 * radius],
['z']
];
return componentsToPath(circlePath);
};
}
inherits(CustomRenderer, BaseRenderer);
module.exports = CustomRenderer;
CustomRenderer.$inject = [ 'eventBus', 'styles' ];
CustomRenderer.prototype.canRender = function(element) {
return /^custom\:/.test(element.type);
};
CustomRenderer.prototype.drawShape = function(visuals, element) {
var type = element.type;
var h = this.handlers[type];
/* jshint -W040 */
return h(visuals, element);
};
CustomRenderer.prototype.drawConnection = function(visuals, element) {
var type = element.type;
var h = this.handlers[type];
/* jshint -W040 */
return h(visuals, element);
};
CustomRenderer.prototype.getShapePath = function(element) {
var type = element.type.replace(/^custom\:/, '');
var shapes = {
triangle: this.getTrianglePath,
circle: this.getCirclePath
};
return shapes[type](element);
};

View File

@ -0,0 +1,148 @@
'use strict';
var forEach = require('lodash/collection/forEach'),
inherits = require('inherits');
var RuleProvider = require('diagram-js/lib/features/rules/RuleProvider');
var HIGH_PRIORITY = 1500;
function isType(element, type) {
var patt = new RegExp(type, 'i');
return element && patt.test(element.type);
}
function isCustom(element) {
return element && /^custom\:/.test(element.type);
}
/**
* Specific rules for custom elements
*/
function CustomRules(eventBus) {
RuleProvider.call(this, eventBus);
}
inherits(CustomRules, RuleProvider);
CustomRules.$inject = [ 'eventBus' ];
module.exports = CustomRules;
CustomRules.prototype.init = function() {
this.addRule('connection.create', HIGH_PRIORITY, function(context) {
var source = context.source,
target = context.target;
return canConnect(source, target);
});
this.addRule('connection.reconnectStart', HIGH_PRIORITY, function(context) {
var connection = context.connection,
source = context.hover || context.source,
target = connection.target;
return canConnect(source, target, connection);
});
this.addRule('connection.reconnectEnd', HIGH_PRIORITY, function(context) {
var connection = context.connection,
source = connection.source,
target = context.hover || context.target;
return canConnect(source, target, connection);
});
this.addRule('connection.updateWaypoints', HIGH_PRIORITY, function(context) {
// OK! but visually ignore
return null;
});
this.addRule('elements.move', HIGH_PRIORITY, function(context) {
var target = context.target,
shapes = context.shapes,
position = context.position;
return canMove(shapes, target, position);
});
this.addRule('shape.create', HIGH_PRIORITY, function(context) {
var target = context.target,
shape = context.shape,
position = context.position;
return canCreate(shape, target, position);
});
this.addRule('shape.resize', HIGH_PRIORITY, function(context) {
var shape = context.shape;
if (isCustom(shape)) {
return false;
}
});
};
function canConnect(source, target) {
if (isType(target, 'custom:triangle')) {
return true;
}
if (isType(target, 'custom:circle')) {
if (isType(source, 'custom:triangle')) {
return true;
}
return false;
}
if (isCustom(source)) {
return true;
}
}
function canCreate(shape, target) {
if (isType(target, 'custom:triangle')) {
return false;
}
if (isType(target, 'custom:circle')) {
if (isType(shape, 'custom:triangle')) {
return true;
}
return false;
}
if (isCustom(shape)) {
return true;
}
}
function canMove(shapes, target, position) {
var result;
forEach(shapes, function(shape) {
if (isType(shape, 'custom:triangle') && isType(target, 'custom:circle')) {
result = true;
return false;
}
if (isCustom(target)) {
result = false;
return false;
}
if (isCustom(shape)) {
result = true;
return false;
}
});
return result;
}

View File

@ -0,0 +1,91 @@
'use strict';
var inherits = require('inherits');
var isBpmn = require('../../../lib/util/ModelUtil').is;
var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor');
function isCustom(element, type) {
return element && element.type === type;
}
function ifCustomElement(fn) {
return function(event) {
var context = event.context,
element = context.shape || context.connection;
if (!isBpmn(element, 'bpmn:BaseElement')) {
fn(event);
}
};
}
/**
* A handler responsible for updating the custom element's businessObject
* once changes on the diagram happen
*/
function CustomUpdater(eventBus, bpmnFactory, connectionDocking) {
CommandInterceptor.call(this, eventBus);
function updateTriangle(evt) {
var context = evt.context,
shape = context.shape,
businessObject = shape.businessObject,
leader = businessObject.leader,
companions,
parent,
idx;
if (!isCustom(shape, 'custom:triangle')) {
return;
}
parent = shape.parent;
if (!parent) {
return;
}
if (isBpmn(parent, 'bpmn:SubProcess')) {
shape.businessObject.foo = 'geil';
}
if (!isBpmn(parent, 'bpmn:SubProcess')) {
shape.businessObject.foo = 'bar';
}
if (isCustom(parent, 'custom:circle')) {
shape.businessObject.leader = parent;
if (!parent.businessObject.companions) {
parent.businessObject.companions = [];
}
parent.businessObject.companions.push(shape);
}
if (!isCustom(parent, 'custom:circle') && leader) {
companions = leader.businessObject.companions;
idx = companions.indexOf(shape);
companions.splice(idx, 1);
businessObject.leader = '';
}
}
this.executed([
'shape.move',
'shape.create'
], ifCustomElement(updateTriangle));
}
inherits(CustomUpdater, CommandInterceptor);
module.exports = CustomUpdater;
CustomUpdater.$inject = [ 'eventBus' ];

View File

@ -0,0 +1,7 @@
module.exports = {
__init__: [ 'customRenderer', 'customRules', 'customUpdater' ],
elementFactory: [ 'type', require('./CustomElementFactory') ],
customRenderer: [ 'type', require('./CustomRenderer') ],
customRules: [ 'type', require('./CustomRules') ],
customUpdater: [ 'type', require('./CustomUpdater') ]
};