UNPKG

jointjs

Version:

JavaScript diagramming library

1,386 lines (1,008 loc) 71.4 kB
QUnit.module('links', function(hooks) { hooks.beforeEach(function() { var $fixture = $('<div>', { id: 'qunit-fixture' }).appendTo(document.body); var $paper = $('<div/>'); $fixture.append($paper); this.graph = new joint.dia.Graph; this.paper = new joint.dia.Paper({ el: $paper, gridSize: 10, model: this.graph }); }); hooks.afterEach(function() { this.paper.remove(); this.graph = null; this.paper = null; }); QUnit.module('isElement()', function(hooks) { QUnit.test('should be a function', function(assert) { assert.equal(typeof joint.dia.Link.prototype.isElement, 'function'); }); QUnit.test('should return FALSE', function(assert) { var link = new joint.dia.Link; assert.notOk(link.isElement()); }); }); QUnit.module('isLink()', function(hooks) { QUnit.test('should be a function', function(assert) { assert.equal(typeof joint.dia.Link.prototype.isLink, 'function'); }); QUnit.test('should return TRUE', function(assert) { var link = new joint.dia.Link; assert.ok(link.isLink()); }); }); QUnit.test('construction', function(assert) { var r1 = new joint.shapes.basic.Rect({ position: { x: 20, y: 30 }, size: { width: 120, height: 80 }}); var r2 = r1.clone().translate(300); this.graph.addCell([r1, r2]); var l0 = new joint.dia.Link({ source: { id: r1.id }, target: { id: r2.id }, attrs: { '.connection': { stroke: 'black' }} }); this.graph.addCell(l0); assert.strictEqual(l0.constructor, joint.dia.Link, 'link.constructor === joint.dia.Link'); var v0 = this.paper.findViewByModel(l0); assert.checkDataPath(v0.$('.connection').attr('d'), 'M 140 70 L 320 70', 'link path data starts at the source right-middle point and ends in the target left-middle point'); var l1 = new joint.dia.Link({ source: { id: r1.id }, target: { id: r2.id }, markup: '<path class="connection"/>' }); assert.ok(_.isUndefined(l1.get('source').x) && _.isUndefined(l1.get('source').y), 'Source connected to an element has no x or y.'); assert.ok(_.isUndefined(l1.get('target').x) && _.isUndefined(l1.get('target').y), 'Target connected to an element has no x or y.'); this.graph.addCell(l1); var v1 = this.paper.findViewByModel(l1); assert.ok(v1, 'link with custom markup (1 child) is rendered.'); var l2 = new joint.dia.Link({ source: { id: r1.id }, target: { id: r2.id }, markup: '<path class="connection"/><path class="connection-wrap"/>' }); this.graph.addCell(l2); var v2 = this.paper.findViewByModel(l2); assert.ok(v2, 'link with custom markup (2 children) is rendered.'); // It should be possible to create empty links and set source/target later. var lEmpty = new joint.dia.Link; assert.ok(true, 'creating a link with no source/target does not throw an exception'); var rEmpty = new joint.shapes.basic.Rect; var r2Empty = new joint.shapes.basic.Rect; this.graph.addCells([lEmpty, rEmpty, r2Empty]); lEmpty.set('source', { id: rEmpty.id }); lEmpty.set('target', { id: r2Empty.id }); assert.equal(lEmpty.get('source').id, rEmpty.id, 'source was set correctly on a blank link'); assert.equal(lEmpty.get('target').id, r2Empty.id, 'target was set correctly on a blank link'); }); QUnit.module('validation', function() { QUnit.test('sanity', function(assert) { var paper = this.paper; var graph = this.graph; var r1, r2, l0; r1 = new joint.shapes.standard.Rectangle; r1.position(0, 0); r1.size(100, 100); r1.addPort({ id: 'port', attrs: { circle: { magnet: true }}}); r2 = r1.clone(); r2.translate(200); l0 = new joint.shapes.standard.Link; l0.source(r1); l0.target(r2); graph.addCells([r1, r2, l0]); var lv0 = l0.findView(paper); var rv1 = r1.findView(paper); var rv2 = r2.findView(paper); var r1port = rv1.el.querySelector('circle'); var r2port = rv2.el.querySelector('circle'); var spy = paper.options.validateConnection = sinon.spy(function() { return true; }); var evt; evt = { type: 'mousemove' }; lv0.startArrowheadMove('source'); evt.target = paper.el; lv0.pointermove(evt, 1000, 1000); evt.target = rv1.el; lv0.pointermove(evt, 50, 50); lv0.pointerup(evt, 50, 50); assert.ok(spy.calledOnce); assert.ok(spy.calledWithExactly(rv1, undefined, rv2, undefined, 'source', lv0)); spy.resetHistory(); lv0.startArrowheadMove('source'); evt.target = paper.el; lv0.pointermove(evt, 1000, 1000); evt.target = r1port; lv0.pointermove(evt, 5, 5); lv0.pointerup(evt, 50, 50); assert.ok(spy.calledOnce); assert.ok(spy.calledWithExactly(rv1, r1port, rv2, undefined, 'source', lv0)); spy.resetHistory(); lv0.startArrowheadMove('target'); evt.target = paper.el; lv0.pointermove(evt, 1000, 1000); evt.target = rv2.el; lv0.pointermove(evt, 5, 5); lv0.pointerup(evt, 50, 50); assert.ok(spy.calledOnce); assert.ok(spy.calledWithExactly(rv1, r1port, rv2, undefined, 'target', lv0)); spy.resetHistory(); lv0.startArrowheadMove('target'); evt.target = paper.el; lv0.pointermove(evt, 1000, 1000); evt.target = r2port; lv0.pointermove(evt, 5, 5); lv0.pointerup(evt, 50, 50); assert.ok(spy.calledOnce); assert.ok(spy.calledWithExactly(rv1, r1port, rv2, r2port, 'target', lv0)); spy.resetHistory(); }); }); QUnit.test('interaction', function(assert) { assert.expect(6); var event; var r1 = new joint.shapes.basic.Rect({ position: { x: 20, y: 30 }, size: { width: 120, height: 80 }}); var r2 = r1.clone().translate(300); var r3 = r2.clone().translate(300); this.graph.addCell([r1, r2, r3]); var vr1 = this.paper.findViewByModel(r1); var vr3 = this.paper.findViewByModel(r3); var l0 = new joint.dia.Link({ source: { id: r1.id }, target: { id: r2.id }, attrs: { '.connection': { stroke: 'black' }}, labels: [ { position: .5, attrs: { text: { text: 'test label' }}} ] }); this.graph.addCell(l0); var v0 = this.paper.findViewByModel(l0); this.paper.options.validateConnection = function(vs, ms, vt, mt, v) { assert.ok(vs === vr1 && vt === vr3, 'connection validation executed'); return vt instanceof joint.dia.ElementView; }; // adding vertices event = { target: v0.el.querySelector('.connection') }; v0.pointerdown(event, 200, 70); v0.pointerup(event); assert.deepEqual(l0.get('vertices'), [{ x: 200, y: 70 }], 'vertex added after click the connection.'); var firstVertexRemoveArea = v0.el.querySelector('.marker-vertex-remove-area'); event = { target: v0.el.querySelector('.connection') }; v0.pointerdown(event, 300, 70); v0.pointermove(event, 300, 100); v0.pointerup(event); assert.deepEqual(l0.get('vertices'), [{ x: 200, y: 70 }, { x: 300, y: 100 }], 'vertex added and translated after click the connection wrapper and mousemove.'); event = { target: firstVertexRemoveArea }; v0.pointerdown(event); v0.pointerup(event); // arrowheadmove var highlighted = false; this.paper.on('cell:highlight', function(cellView, el) { if (el[0] === vr3.el[0]) { highlighted = true; } }); this.paper.on('cell:unhighlight', function(cellView, el) { if (el[0] === vr3.el[0]) { highlighted = false; } }); event = { target: v0.el.querySelector('.marker-arrowhead[end="target"]') }; v0.pointerdown(event); event.target = vr3.el; event.type = 'mousemove'; v0.pointermove(event, 630, 40); assert.ok(highlighted, 'moving pointer over the rectangle makes the rectangle highlighted'); event.target = this.paper.el; v0.pointermove(event, 400, 400); assert.notOk(highlighted, 'after moving the pointer to coordinates 400, 400 the rectangle is not highlighted anymore'); v0.pointerup(event); assert.checkDataPath(v0.el.querySelector('.connection').getAttribute('d'), 'M 140 78 L 300 100 L 400 400', 'link path data starts at the source right-middle point, going through the vertex and ends at the coordinates 400, 400'); }); QUnit.test('defaultLink', function(assert) { assert.expect(10); this.paper.options.defaultLink = new joint.dia.Link(); var link = this.paper.getDefaultLink(); assert.ok(link instanceof joint.dia.Link, 'sanity: defaultLink is cloned'); this.paper.options.defaultLink = function(v, m) { return new joint.dia.Link(); }; link = this.paper.getDefaultLink(); assert.ok(link instanceof joint.dia.Link, 'sanity: defaultLink is a function'); var MyLink = joint.dia.Link.extend({ isMyLink: true }); var model = new joint.shapes.basic.Rect({ position: { x: 100, y: 100 }, size: { width: 100, height: 100 }, attrs: { rect: { magnet: true, port: 'myPort' }} }); this.graph.addCell(model); var view = this.paper.findViewByModel(model); var rect = view.$('rect')[0]; this.graph.on('add', function(cell) { assert.ok(cell.isMyLink, 'We click the port and a default link was created.'); // check source of the link var source = cell.get('source'); assert.ok(source.id === model.id && source.port === 'myPort', 'It starts in the port we clicked.'); // check target of the link var target = cell.get('target'); assert.ok(typeof target.id === 'undefined', 'It ends in the paper.'); }); this.paper.options.defaultLink = new MyLink(); simulate.mousedown({ el: rect }); simulate.mouseup({ el: rect }); link = this.graph.getLinks()[0]; var linkView = link.findView(this.paper); assert.equal(linkView.sourceMagnet, rect); link.remove(); this.paper.options.defaultLink = function(cellView, magnet) { assert.ok(cellView === view && magnet === rect, 'We set defaultLink to a function. It was executed with correct parameters.'); return new MyLink(); }; simulate.mousedown({ el: rect }); simulate.mouseup({ el: rect }); }); QUnit.test('source', function(assert) { var link = new joint.dia.Link({ source: { x: 40, y: 40 }, target: { x: 100, y: 100 } }); this.graph.addCell(link); assert.equal(typeof link.source, 'function', 'should be a function'); var source; source = link.source(); assert.deepEqual(source, { x: 40, y: 40 }, 'source is a correct point'); var element = new joint.shapes.basic.Rect({ position: { x: 20, y: 20 }, size: { width: 60, height: 60 } }); this.graph.addCell(element); link.source(element); source = link.source(); assert.deepEqual(source, { id: element.id }, 'source is a correct element'); assert.equal(source.id, link.getSourceElement().id, 'source element ID is correct'); }); QUnit.test('target', function(assert) { var link = new joint.dia.Link({ source: { x: 40, y: 40 }, target: { x: 100, y: 100 } }); this.graph.addCell(link); assert.equal(typeof link.target, 'function', 'should be a function'); var target; target = link.target(); assert.deepEqual(target, { x: 100, y: 100 }, 'target is a correct point'); var element = new joint.shapes.basic.Rect({ position: { x: 20, y: 20 }, size: { width: 60, height: 60 } }); this.graph.addCell(element); link.target({ id: element.id }); target = link.target(); assert.deepEqual(target, { id: element.id }, 'target is a correct element'); assert.equal(target.id, link.getTargetElement().id, 'target element ID is correct'); }); QUnit.test('disconnect(), connect()', function(assert) { var myrect = new joint.shapes.basic.Rect({ position: { x: 20, y: 30 }, size: { width: 120, height: 80 }, attrs: { text: { text: 'my rectangle' }} }); this.graph.addCell(myrect); var myrect2 = myrect.clone(); myrect2.translate(300); this.graph.addCell(myrect2); var link = new joint.dia.Link({ source: { id: myrect.id }, target: { id: myrect2.id }, attrs: { '.connection': { stroke: 'black' }} }); this.graph.addCell(link); var linkView = this.paper.findViewByModel(link); // disconnect: link.set('source', linkView.sourcePoint.toJSON()); assert.notOk(link.get('source').id, 'source of the link became a point'); assert.ok(link.get('target').id, 'target of the link is still not a point'); assert.checkDataPath(linkView.$('.connection').attr('d'), 'M 140 70 L 320 70', 'link path data stayed the same after disconnection'); assert.checkDataPath(linkView.$('.connection-wrap').attr('d'), 'M 140 70 L 320 70', 'link connection-wrap path data is the same as the .connection path data'); myrect.translate(-10); assert.checkDataPath(linkView.$('.connection').attr('d'), 'M 140 70 L 320 70', 'link path data stayed the same after the disconnected source moved'); link.set('source', { id: myrect.id }); assert.checkDataPath(linkView.$('.connection').attr('d'), 'M 130 70 L 320 70', 'link path data updated after the disconnected source became re-connected again'); myrect.translate(10); assert.checkDataPath(linkView.$('.connection').attr('d'), 'M 140 70 L 320 70', 'link path data updated after the just connected source moved'); // disconnect: link.set('target', linkView.targetPoint.toJSON()); assert.notOk(link.get('target').id, 'target of the link became a point'); assert.ok(link.get('source').id, 'source of the link is still not a point'); link.set('target', { id: myrect2.id }); assert.ok(link.get('source').id, 'source of the link is still not a point'); assert.ok(link.get('target').id, 'target of the link stopped being a point'); myrect.remove({ disconnectLinks: true }); assert.notOk(link.get('source').id, 'source of the link became a point after the source element has been removed'); assert.ok(link.get('target').id, 'target of the link is still not a point'); }); QUnit.test('getLinks(), clone()', function(assert) { var myrect = new joint.shapes.basic.Rect; var myrect2 = myrect.clone(); this.graph.addCell(myrect); this.graph.addCell(myrect2); var link = new joint.dia.Link({ source: { id: myrect.id }, target: { id: myrect2.id } }); var link2 = link.clone(); this.graph.addCell(link); this.graph.addCell(link2); assert.deepEqual(_.map(this.graph.getConnectedLinks(myrect), 'id'), [link.id, link2.id], 'getConnectedLinks() returns both links comming out of the source element'); assert.deepEqual(_.map(this.graph.getConnectedLinks(myrect2), 'id'), [link.id, link2.id], 'getConnectedLinks() returns both links leading to the target element'); link.disconnect(); assert.deepEqual(_.map(this.graph.getConnectedLinks(myrect), 'id'), [link2.id], 'getConnectedLinks() returns only one link coming out of it after the other has been disconnected'); assert.deepEqual(_.map(this.graph.getConnectedLinks(myrect2), 'id'), [link2.id], 'getConnectedLinks() returns only one link leading to it after the other has been disconnected'); assert.deepEqual(_.map(this.graph.getConnectedLinks(myrect, { outbound: true }), 'id'), [link2.id], 'getConnectedLinks(outbound) returns only the one link coming out the element'); assert.deepEqual(_.map(this.graph.getConnectedLinks(myrect, { inbound: true }), 'id'), [], 'getConnectedLinks(inbound) returns no link as the element is not source of any link'); }); QUnit.test('hasLoop()', function(assert) { var myrect = new joint.shapes.basic.Rect; this.graph.addCell(myrect); var link = new joint.dia.Link({ source: { id: myrect.id }, target: { id: myrect.id }}); this.graph.addCell(link); assert.equal(link.hasLoop(), true, 'link has a loop'); var myrect2 = new joint.shapes.basic.Rect; this.graph.addCell(myrect2); var link2 = new joint.dia.Link({ source: { id: myrect2.id }, target: { x: 20, y: 20 }}); this.graph.addCell(link2); assert.equal(link2.hasLoop(), false, 'link pinned to the paper does not have a loop'); assert.equal(link2.hasLoop({ deep: true }), false, 'link pinned to the paper does not have a loop with deep = true'); var myrect3 = new joint.shapes.basic.Rect; var myrect3a = new joint.shapes.basic.Rect; myrect3.embed(myrect3a); this.graph.addCells([myrect3, myrect3a]); var link3 = new joint.dia.Link({ source: { id: myrect3.id }, target: { id: myrect3a.id }}); this.graph.addCell(link3); assert.equal(link3.hasLoop(), false, 'link targetting an embedded element does not have a loop with deep = false'); assert.equal(link3.hasLoop({ deep: true }), true, 'link targetting an embedded element does have a loop with deep = true'); }); QUnit.test('markers', function(assert) { var myrect = new joint.shapes.basic.Rect({ position: { x: 20, y: 30 }, size: { width: 120, height: 80 }, attrs: { text: { text: 'my rectangle' }} }); var myrect2 = myrect.clone(); myrect2.translate(300); this.graph.addCell(myrect); this.graph.addCell(myrect2); var link = new joint.dia.Link({ source: { id: myrect.id }, target: { id: myrect2.id }, attrs: { '.connection': { stroke: 'black' }, '.marker-source': { d: 'M 10 0 L 0 5 L 10 10 z' }, '.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z' } } }); this.graph.addCell(link); var linkView = this.paper.findViewByModel(link); var markerSourceBbox = V(linkView.$('.marker-source')[0]).bbox(); assert.deepEqual( { x: markerSourceBbox.x, y: markerSourceBbox.y, width: markerSourceBbox.width, height: markerSourceBbox.height }, { x: 140, y: 65, width: 10, height: 10 }, '.marker-source should point to the left edge of the rectangle' ); var markerTargetBbox = V(linkView.$('.marker-target')[0]).bbox(); assert.deepEqual( { x: markerTargetBbox.x, y: markerTargetBbox.y, width: markerTargetBbox.width, height: markerTargetBbox.height, rotation: V(linkView.$('.marker-target')[0]).rotate().angle }, { x: 310, y: 65, width: 10, height: 10, rotation: -180 }, '.marker-target should point to the right edge of the rectangle 2 and should be rotated by -180 degrees' ); }); QUnit.test('vertices', function(assert) { var myrect = new joint.shapes.basic.Rect({ position: { x: 20, y: 30 }, size: { width: 120, height: 80 }, attrs: { text: { text: 'my rectangle' }} }); var myrect2 = myrect.clone(); myrect2.translate(300); this.graph.addCell(myrect); this.graph.addCell(myrect2); var link = new joint.dia.Link({ source: { id: myrect.id }, target: { id: myrect2.id }, vertices: [{ x: 80, y: 150 }, { x: 380, y: 150 }], attrs: { '.connection': { stroke: 'black' }, '.marker-source': { d: 'M 10 0 L 0 5 L 10 10 z' }, '.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z' } } }); this.graph.addCell(link); var linkView = this.paper.findViewByModel(link); var markerSourceBbox = V(linkView.$('.marker-source')[0]).bbox(); assert.deepEqual( { x: markerSourceBbox.x, y: markerSourceBbox.y, width: markerSourceBbox.width, height: markerSourceBbox.height, rotation: g.normalizeAngle(V(linkView.$('.marker-source')[0]).rotate().angle) }, { x: 75, y: 110, width: 10, height: 10, rotation: g.normalizeAngle(-270) }, '.marker-source should point to the bottom edge of the rectangle and should be rotated by -270 degrees' ); var markerTargetBbox = V(linkView.$('.marker-target')[0]).bbox(); assert.deepEqual( { x: markerTargetBbox.x, y: markerTargetBbox.y, width: markerTargetBbox.width, height: markerTargetBbox.height, rotation: g.normalizeAngle(V(linkView.$('.marker-target')[0]).rotate().angle) }, { x: 375, y: 110, width: 10, height: 10, rotation: g.normalizeAngle(-270) }, '.marker-target should point to the bottom edge of the rectangle 2 and should be rotated by -270 degrees' ); assert.equal($('.marker-vertex').length, 2, 'there is exactly 2 vertex markers on the page'); var firstVertextPosition = g.rect(V($('.marker-vertex')[0]).bbox()).center(); assert.deepEqual( { x: firstVertextPosition.x, y: firstVertextPosition.y }, link.get('vertices')[0], 'first vertex is in the same position as defined in the vertices array' ); var secondVertextPosition = g.rect(V($('.marker-vertex')[1]).bbox()).center(); assert.deepEqual( { x: secondVertextPosition.x, y: secondVertextPosition.y }, link.get('vertices')[1], 'second vertex is in the same position as defined in the vertices array' ); }); QUnit.test('perpendicularLinks', function(assert) { this.paper.options.perpendicularLinks = true; var myrect = new joint.shapes.basic.Rect({ position: { x: 20, y: 30 }, size: { width: 120, height: 80 }, attrs: { text: { text: 'my rectangle' }} }); var myrect2 = myrect.clone(); myrect2.translate(300); this.graph.addCell(myrect); this.graph.addCell(myrect2); var link = new joint.dia.Link({ source: { id: myrect.id }, target: { id: myrect2.id }, vertices: [{ x: 138, y: 150 }, { x: 180, y: 108 }], attrs: { '.connection': { stroke: 'black' }, '.marker-source': { d: 'M 0 0 L 0 0 z' }, '.marker-target': { d: 'M 0 0 L 0 0 z' } } }); this.graph.addCell(link); var linkView = this.paper.findViewByModel(link); var markerSourceBbox = V(linkView.$('.marker-source')[0]).bbox(); assert.deepEqual( { x: markerSourceBbox.x, y: markerSourceBbox.y }, { x: 138, y: 110 }, '.marker-source should point vertically to the edge of the source rectangle making the part of the link before the first vertex perpendicular to the source rectangle' ); var markerTargetBbox = V(linkView.$('.marker-target')[0]).bbox(); assert.deepEqual( { x: markerTargetBbox.x, y: markerTargetBbox.y }, { x: myrect2.get('position').x, y: 108 }, '.marker-target should point horizontally to the edge of the target rectangle making the part of the link after the last vertex perpendicular to the target rectangle' ); }); QUnit.module('Labels', function(assert) { QUnit.test('labels', function(assert) { var myrect = new joint.shapes.basic.Rect; var myrect2 = myrect.clone(); myrect2.translate(300); this.graph.addCell(myrect); this.graph.addCell(myrect2); var link = new joint.dia.Link({ source: { id: myrect.id }, target: { id: myrect2.id }, labels: [ { position: 10, attrs: { text: { text: '1..n' }}}, { position: .5, attrs: { text: { text: 'Foo', fill: 'white', 'font-family': 'sans-serif' }, rect: { stroke: '#F39C12', 'stroke-width': 20, rx: 5, ry: 5 }}}, { position: -10, attrs: { text: { text: '*' }}} ] }); this.graph.addCell(link); var linkView = this.paper.findViewByModel(link); assert.equal(linkView.$('.label').length, 3, 'label elements were correctly added to the DOM'); var label1Bbox = V(linkView.$('.label')[0]).bbox(); var label2Bbox = V(linkView.$('.label')[1]).bbox(); var label3Bbox = V(linkView.$('.label')[2]).bbox(); assert.ok(label1Bbox.x < label2Bbox.x, 'second label is positioned after the first one'); assert.ok(label2Bbox.x < label3Bbox.x, 'third label is positioned after the second one'); assert.equal(linkView.$('.label')[0].textContent, '1..n', 'first label has correctly set text'); assert.equal(linkView.$('.label')[1].textContent, 'Foo', 'second label has correctly set text'); assert.equal(linkView.$('.label')[2].textContent, '*', 'third label has correctly set text'); link.label(1, { attrs: { text: { text: 'Bar' }}}); assert.equal(linkView.$('.label')[1].textContent, 'Bar', 'a call to link.label() changed text of the second label correctly'); link.label(0, { position: -10 }); label1Bbox = V(linkView.$('.label')[0]).bbox(); assert.ok(label1Bbox.x > label2Bbox.x, 'second label is positioned before the first one after changing the first one position'); }); QUnit.test('labelMove', function(assert) { assert.expect(2); var r1 = new joint.shapes.basic.Rect({ position: { x: 50, y: 50 }, size: { width: 50, height: 50 }}); var r2 = r1.clone().translate(250); this.graph.addCell([r1, r2]); var l0 = new joint.dia.Link({ source: { id: r1.id }, target: { id: r2.id }, attrs: { '.connection': { stroke: 'black' }}, labels: [ { position: .5, attrs: { text: { text: 'test label' }}} ] }); this.graph.addCell(l0); var v0 = this.paper.findViewByModel(l0); v0.options.interactive = { labelMove: true }; var event = { currentTarget: v0.$('.label')[0], type: 'mousedown' }; v0.dragLabelStart(event); v0.pointermove(event, 150, 25); assert.equal(l0.get('labels')[0].position.offset, -50, 'offset was set during the label drag'); assert.equal(l0.get('labels')[0].position.distance, .25, 'distance was set during the label drag'); v0.pointerup(event); }); QUnit.test('change:labels', function(assert) { var l = new joint.dia.Link({ source: { x: 0, y: 0 }, target: { x: 100, y: 100 } }).addTo(this.graph); var view = l.findView(this.paper); var renderSpy = sinon.spy(view, 'renderLabels'); var updateSpy = sinon.spy(view, 'updateLabels'); l.set({ labels: [ { position: 20, attrs: { text: { text: 'label1' }}}, { position: -20, attrs: { text: { text: 'label2' }}} ] }); assert.ok(renderSpy.calledOnce); assert.ok(updateSpy.calledOnce); renderSpy.resetHistory(); updateSpy.resetHistory(); l.prop('labels/0/attrs/text/text', 'label3', { rewrite: true }); assert.ok(renderSpy.notCalled); assert.ok(updateSpy.calledOnce); renderSpy.resetHistory(); updateSpy.resetHistory(); l.prop('labels/0', { attrs: { text: { text: 'label4' }}}, { rewrite: true }); assert.ok(renderSpy.notCalled); assert.ok(updateSpy.calledOnce); renderSpy.resetHistory(); updateSpy.resetHistory(); l.prop('labels/1', { markup: '<rect/><text/>' }, { rewrite: true }); assert.ok(renderSpy.calledOnce); assert.ok(updateSpy.calledOnce); renderSpy.resetHistory(); updateSpy.resetHistory(); l.prop('labels/0/markup', '<rect/><text/>', { rewrite: true }); assert.ok(renderSpy.calledOnce); assert.ok(updateSpy.calledOnce); renderSpy.resetHistory(); updateSpy.resetHistory(); l.prop('labels/1', { markup: [{ tagName: 'rect' }, { tagName: 'text' }] }, { rewrite: true }); assert.ok(renderSpy.calledOnce); assert.ok(updateSpy.calledOnce); renderSpy.resetHistory(); updateSpy.resetHistory(); l.prop('labels/0/markup', [{ tagName: 'rect' }, { tagName: 'text' }], { rewrite: true }); assert.ok(renderSpy.calledOnce); assert.ok(updateSpy.calledOnce); renderSpy.resetHistory(); updateSpy.resetHistory(); l.prop('labels/1', { markup: [{ tagName: 'rect', selector: 'body' }, { tagName: 'text', selector: 'label' }] }, { rewrite: true }); assert.ok(renderSpy.calledOnce); assert.ok(updateSpy.calledOnce); renderSpy.resetHistory(); updateSpy.resetHistory(); l.prop('labels/0/markup', [{ tagName: 'rect', selector: 'body' }, { tagName: 'text', selector: 'label' }], { rewrite: true }); assert.ok(renderSpy.calledOnce); assert.ok(updateSpy.calledOnce); renderSpy.resetHistory(); updateSpy.resetHistory(); l.prop('labels/0/attrs/label/text', 'label3', { rewrite: true }); assert.ok(renderSpy.notCalled); assert.ok(updateSpy.calledOnce); renderSpy.resetHistory(); updateSpy.resetHistory(); l.prop('labels/0', { attrs: { label: { text: 'label4' }}}, { rewrite: true }); assert.ok(renderSpy.notCalled); assert.ok(updateSpy.calledOnce); renderSpy.resetHistory(); updateSpy.resetHistory(); }); QUnit.test('change:labels + change:source', function(assert) { var graph = this.graph; var paper = this.paper; var l = new joint.dia.Link({ source: { x: 0, y: 0 }, target: { x: 100, y: 100 } }).addTo(graph); var view = l.findView(paper); assert.notOk(view.el.querySelector('.labels')); paper.freeze(); l.set({ source: { x: 20, y: 20 }, labels: [ { position: 20, attrs: { text: { text: 'label1' }}}, { position: -20, attrs: { text: { text: 'label2' }}} ] }); paper.unfreeze(); assert.ok(view.el.querySelector('.labels')); }); }); QUnit.test('magnets & ports', function(assert) { var myrect = new joint.shapes.basic.Rect; var myrect2 = myrect.clone(); myrect2.translate(300); this.graph.addCells([myrect, myrect2]); myrect.attr('text', { magnet: true, port: 'port1' }); myrect2.attr('text', { magnet: true, port: 'port2' }); var myrectView = this.paper.findViewByModel(myrect); var myrect2View = this.paper.findViewByModel(myrect2); simulate.mousedown({ el: myrectView.$('text')[0] }); simulate.mousemove({ el: myrect2View.$('text')[0] }); simulate.mouseup({ el: myrect2View.$('text')[0] }); var link = this.graph.getLinks()[0]; var linkView = link.findView(this.paper); assert.equal(link.get('source').port, 'port1', 'port was automatically assigned to the link source'); assert.equal(link.get('target').port, 'port2', 'port was automatically assigned to the link target'); assert.equal(linkView.sourceMagnet, myrectView.$('text')[0], 'source selector points to the magnet element'); assert.equal(linkView.targetMagnet, myrect2View.$('text')[0], 'target selector points to the magnet element'); // The functionality below is not implemented, hence skiping the test. // myrect.attr('text', { port: 'port3' }); // equal(link.get('source').port, 'port3', 'changing port on an element automatically changes the same port on a link'); }); QUnit.test('snap links', function(assert) { var event; var link = new joint.dia.Link({ source: { x: 0, y: 0 }, target: { x: 0, y: 0 } }); var myrect = new joint.shapes.basic.Rect({ position: { x: 100, y: 100 } }); this.graph.addCells([myrect, link]); var v = this.paper.findViewByModel(link); var t = v.el.querySelector('.marker-arrowhead[end=target]'); // link target was out of the radius and therefore was not snapped to the element this.paper.options.snapLinks = { radius: 5 }; event = { target: t }; v.pointerdown(event, 0, 0); event.target = this.paper.el; v.pointermove(event, 90, 90); v.pointerup(event, 90, 90); assert.deepEqual(link.get('target'), { x: 90, y: 90 }, 'link target was out of the radius and therefore was not snapped to the element'); // link target was snapped to the element this.paper.options.snapLinks = { radius: 50 }; event = { target: t }; v.pointerdown(event, 0, 0); event.target = this.paper.el; v.pointermove(event, 90, 90); v.pointerup(event, 90, 90); assert.ok(link.get('target').id === myrect.id, 'link target was snapped to the element'); // link target was snapped to the port // getBoundingClientRect returns negative values for top and left when paper not visible this.paper.options.snapLinks = { radius: Number.MAX_VALUE }; myrect.attr('.', { magnet: false }); myrect.attr('text', { magnet: true, port: 'port' }); this.paper.options.validateConnection = function() { return true; }; event = { target: t }; v.pointerdown(event, 0, 0); event.target = this.paper.el; v.pointermove(event, 90, 90); v.pointerup(event, 90, 90); assert.ok(link.get('target').port === 'port', 'link target was snapped to the port'); // the validation is taken into account when snapping to port this.paper.options.validateConnection = function() { return false; }; event = { target: t }; v.pointerdown(event, 0, 0); event.target = this.paper.el; v.pointermove(event, 90, 90); v.pointerup(event, 90, 90); assert.deepEqual(link.get('target'), { x: 90, y: 90 }, 'the validation is taken into account when snapping to port'); }); QUnit.test('mark available', function(assert) { var event; var link = new joint.dia.Link({ source: { x: 0, y: 0 }, target: { x: 0, y: 0 } }); var myrect1 = new joint.shapes.basic.Rect({ position: { x: 100, y: 100 } }); var myrect2 = new joint.shapes.basic.Rect({ position: { x: 200, y: 200 }, attrs: { '.': { magnet: false }, rect: { magnet: true }, text: { magnet: true } } }); this.graph.addCells([myrect1, myrect2, link]); var v = this.paper.findViewByModel(link); var t = v.el.querySelector('.marker-arrowhead[end=target]'); this.paper.options.markAvailable = true; event = { target: t }; v.pointerdown(event, 0, 0); var availableMagnets = this.paper.el.querySelectorAll('.available-magnet'); var availableCells = this.paper.el.querySelectorAll('.available-cell'); assert.equal(availableMagnets.length, 3, '3 magnets got marked when dragging an arrowhead started.'); assert.equal(availableCells.length, 2, '2 cells got marked when dragging an arrowhead started.'); event.target = this.paper.el; v.pointerup(event, 90, 90); availableMagnets = this.paper.el.querySelectorAll('.available-magnet'); availableCells = this.paper.el.querySelectorAll('.available-cell'); assert.equal(availableMagnets.length + availableCells.length, 0, 'When dragging an arrowhed stopped all magnets and cells were unmarked.'); }); QUnit.test('defaultRouter', function(assert) { assert.expect(1); this.paper.options.defaultRouter = function(vertices) { assert.ok(vertices.length > 0, 'Default router was used for the model with no router defined.'); }; var linkDefaultRouter = new joint.dia.Link({ source: { x: 0, y: 0 }, target: { x: 0, y: 0 }, vertices: [{ x: 50, y: 50 }] }); var linkOwnRouter = new joint.dia.Link({ source: { x: 0, y: 0 }, target: { x: 0, y: 0 }, router: { name: 'orthogonal' }, vertices: [] }); this.graph.addCells([linkDefaultRouter, linkOwnRouter]); }); QUnit.test('defaultConnector', function(assert) { assert.expect(1); this.paper.options.defaultConnector = function(s, t, vertices) { assert.ok(vertices.length > 0, 'Default connector was used for the model with no connector defined.'); return 'M 0 0'; }; var linkDefaultConnector = new joint.dia.Link({ source: { x: 0, y: 0 }, target: { x: 0, y: 0 }, vertices: [{ x: 50, y: 50 }] }); var linkOwnConnector = new joint.dia.Link({ source: { x: 0, y: 0 }, target: { x: 0, y: 0 }, connector: { name: 'normal' }, vertices: [] }); this.graph.addCells([linkDefaultConnector, linkOwnConnector]); }); QUnit.test('getSourceCell', function(assert) { var link = new joint.dia.Link({ source: { x: 40, y: 40 }, target: { x: 100, y: 100 } }); this.graph.addCell(link); assert.equal(typeof link.getSourceCell, 'function', 'should be a function'); var source; source = link.getSourceCell(); assert.equal(source, null, 'without source element'); var element = new joint.shapes.basic.Rect({ position: { x: 20, y: 20 }, size: { width: 60, height: 60 } }); this.graph.addCell(element); link.set('source', { id: element.id }); source = link.getSourceCell(); assert.ok(source && source instanceof joint.dia.Element && source.id === element.id, 'with source element'); var linkNotInGraph = new joint.dia.Link({ source: { id: element.get('id') }, target: { id: element.get('id') } }); var thrownError; try { linkNotInGraph.getSourceCell(); } catch (error) { thrownError = error; } assert.ok(typeof thrownError === 'undefined', 'should not throw an error when link not in graph'); }); QUnit.test('getSourceElement', function(assert) { var link1 = new joint.shapes.standard.Link(); var link2 = new joint.shapes.standard.Link(); this.graph.addCells([link1, link2]); assert.equal(typeof link1.getSourceElement, 'function'); assert.equal(link1.getSourceElement(), null, 'without source element'); assert.equal(link2.getSourceElement(), null, 'without source element'); var element = new joint.shapes.standard.Rectangle(); this.graph.addCell(element); link1.source(element); link2.source(link1); assert.equal(link1.getSourceElement(), element); assert.equal(link2.getSourceElement(), element); }); QUnit.test('getTargetCell', function(assert) { var link = new joint.dia.Link({ source: { x: 40, y: 40 }, target: { x: 100, y: 100 } }); this.graph.addCell(link); assert.equal(typeof link.getTargetCell, 'function', 'should be a function'); var target; target = link.getTargetCell(); assert.equal(target, null, 'without target element'); var element = new joint.shapes.basic.Rect({ position: { x: 20, y: 20 }, size: { width: 60, height: 60 } }); this.graph.addCell(element); link.set('target', { id: element.id }); target = link.getTargetCell(); assert.ok(target && target instanceof joint.dia.Element && target.id === element.id, 'with target element'); var linkNotInGraph = new joint.dia.Link({ source: { id: element.get('id') }, target: { id: element.get('id') } }); var thrownError; try { linkNotInGraph.getTargetCell(); } catch (error) { thrownError = error; } assert.ok(typeof thrownError === 'undefined', 'should not throw an error when link not in graph'); }); QUnit.test('getTargetElement', function(assert) { var link1 = new joint.shapes.standard.Link(); var link2 = new joint.shapes.standard.Link(); this.graph.addCells([link1, link2]); assert.equal(typeof link1.getTargetElement, 'function'); assert.equal(link1.getTargetElement(), null, 'without source element'); assert.equal(link2.getTargetElement(), null, 'without source element'); var element = new joint.shapes.standard.Rectangle(); this.graph.addCell(element); link1.target(element); link2.target(link1); assert.equal(link1.getTargetElement(), element); assert.equal(link2.getTargetElement(), element); }); QUnit.test('getRelationshipAncestor()', function(assert) { var a = new joint.shapes.basic.Rect({ id: 'a' }); var b = new joint.shapes.basic.Rect({ id: 'b' }); var c = new joint.shapes.basic.Rect({ id: 'c' }); var l = new joint.dia.Link({ id: 'l' }); this.graph.addCells([a, b, c, l]); assert.equal(l.getRelationshipAncestor(), null, 'Link has no parent and connects 2 points. No ancestor found.'); l.set('source', { id: 'a' }); assert.equal(l.getRelationshipAncestor(), null, 'Link has no parent and connects a point and an element. No ancestor found.'); l.set('target', { id: 'b' }); assert.equal(l.getRelationshipAncestor(), null, 'Link has no parent and connects 2 elements. No ancestor found.'); c.embed(a).embed(b); assert.equal(l.getRelationshipAncestor(), null, 'Source and target are embedded. No ancestor found.'); c.embed(l); assert.equal(l.getRelationshipAncestor(), c, 'Source, target and link are embedded. Ancestor found.'); c.unembed(a).unembed(b); assert.equal(l.getRelationshipAncestor(), null, 'Only link is embeded. No ancestor found.'); }); QUnit.test('isRelationshipEmbeddedIn()', function(assert) { var a = new joint.shapes.basic.Rect({ id: 'a' }); var b = new joint.shapes.basic.Rect({ id: 'b' }); var c = new joint.shapes.basic.Rect({ id: 'c' }); var l = new joint.dia.Link({ id: 'l' }); this.graph.addCells([a, b, c, l]); assert.notOk(l.isRelationshipEmbeddedIn(c), 'Link has no parent and connects 2 points. The relationship is not embedded.'); c.embed(l); assert.ok(l.isRelationshipEmbeddedIn(c), 'Link is embedded and connects 2 points. The relationship is embedded.'); l.set('source', { id: 'a' }); assert.notOk(l.isRelationshipEmbeddedIn(c), 'Link is embedded and connects a point and an element with no parent. The relationship is not embedded.'); l.set('target', { id: 'b' }); assert.notOk(l.isRelationshipEmbeddedIn(c), 'Link is embedded and connects 2 elements with no parents. The relationship is not embedded.'); c.embed(a).embed(b); assert.ok(l.isRelationshipEmbeddedIn(c), 'Link is embedded and connects 2 elements also embedded. The relationship is embedded.'); }); QUnit.test('update count', function(assert) { var a = new joint.shapes.basic.Rect({ id: 'a' }); var b = new joint.shapes.basic.Rect({ id: 'b' }); var c = new joint.shapes.basic.Rect({ id: 'c' }); var l = new joint.dia.Link({ id: 'l' }); var l2 = new joint.dia.Link({ id: 'l2' }); this.graph.addCells([a, b, c, l, l2]); var lv = l.findView(this.paper); var l2v = l2.findView(this.paper); sinon.spy(lv, 'update'); sinon.spy(lv, 'findRoute'); sinon.spy(l2v, 'update'); l.translate(10, 10); assert.equal(lv.update.callCount, 1, 'update: link point to point, link translated'); assert.equal(lv.findRoute.callCount, 1, 'findRoute: link point to point, link translated'); l.set('target', { id: 'a' }); lv.update.resetHistory(); l.translate(10, 10); assert.equal(lv.update.callCount, 1, 'update: link point to element, source translated'); l.set('target', { x: 0, y: 0 }); l.set('source', { id: 'a' }); lv.update.resetHistory(); a.translate(10, 10); assert.equal(lv.update.callCount, 1, 'update: link element to point, source translated'); lv.update.resetHistory(); l.translate(10, 10); assert.equal(lv.update.callCount, 1, 'update: link element to point, link translated'); l.vertices([{ x: 0, y: 0 }]); lv.update.resetHistory(); a.translate(10, 10); assert.equal(lv.update.callCount, 1, 'update: link element to point with vertices, link translated'); // loop l.vertices([]); l.set('target', { id: 'a' }); lv.update.resetHistory(); a.translate(10, 10); assert.equal(lv.update.callCount, 1, 'update: loop link, source translated'); // link element-element l.set('target', { id: 'b' }); lv.update.resetHistory(); a.translate(10, 10); assert.equal(lv.update.callCount, 1, 'update: link element-element, source translated'); l.set('vertices', [{ x: 0, y: 0 }]); lv.update.resetHistory(); a.translate(10, 10); assert.equal(lv.update.callCount, 1, 'update: link element-element with vertices, source translated'); l.set('target', { x: 0, y: 0 }); lv.update.resetHistory(); l.translate(10, 10); assert.equal(lv.update.callCount, 1, 'update: link point-element with vertices, link translated'); // loop + vertices l.set('target', { id: 'a' }); lv.update.resetHistory(); a.translate(10, 10); assert.equal(lv.update.callCount, 1, 'update: loop link with vertices, source translated.'); // embeds // loop + vertices + embedded a.embed(l); lv.update.resetHistory(); a.translate(10, 10); assert.equal(lv.update.callCount, 1, 'update: embedded loop link with vertices, source translated.'); // loop + vertices + embedded (moving container) c.embed(a); lv.update.resetHistory(); c.translate(10, 10); assert.equal(lv.update.callCount, 1, 'update: embedded loop link with vertices, container