jointjs
Version:
JavaScript diagramming library
1,262 lines (927 loc) • 71.1 kB
JavaScript
QUnit.module('basic', function(hooks) {
hooks.beforeEach(function() {
this.$fixture = $('<div>', { id: 'qunit-fixture' }).appendTo(document.body);
var $paper = $('<div/>');
this.$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;
this.$fixture.empty();
this.$fixture = null;
});
this.setupTestNestedGraph = function(graph) {
// make element
function me(id) {
return new joint.shapes.basic.Circle({ id: id, name: id }).addTo(graph);
}
// make link
function ml(id, a, b) {
var source = a.x ? a : { id: a.id };
var target = b.x ? b : { id: b.id };
return new joint.dia.Link({ id: id, source: source, target: target, name: id }).addTo(graph);
}
var a = me('a');
var aa = me('aa');
a.embed(aa);
var aaa = me('aaa');
aa.embed(aaa);
var c = me('c');
a.embed(c);
var d = me('d');
ml('l1', aa, c);
var l2 = ml('l2', aa, aaa);
aa.embed(l2);
ml('l3', c, d);
};
QUnit.test('construction', 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);
assert.strictEqual(myrect.constructor, joint.shapes.basic.Rect, 'myrect.constructor === joint.shapes.basic.Rect');
var textEls = this.paper.svg.getElementsByTagName('text');
var rectEls = this.paper.svg.getElementsByTagName('rect');
assert.equal(textEls.length, 1, 'there is exactly one <text> element in the paper');
assert.equal(rectEls.length, 1, 'there is exactly one <rect> element in the paper');
assert.equal(textEls[0].textContent, V.sanitizeText('my rectangle'), 'text element has a proper content');
});
QUnit.test('async: resetCells', function(assert) {
var done = assert.async();
var r1 = new joint.shapes.basic.Rect({
position: { x: 20, y: 30 },
size: { width: 120, height: 80 },
attrs: { text: { text: 'my rectangle' }}
});
var r2 = r1.clone();
var r3 = r1.clone();
this.paper.options.async = { batchSize: 1 };
this.paper.on('render:done', function() {
var textEls = this.paper.svg.getElementsByTagName('text');
var rectEls = this.paper.svg.getElementsByTagName('rect');
assert.equal(textEls.length, 3, 'there is exactly 3 <text> elements in the paper');
assert.equal(rectEls.length, 3, 'there is exactly 3 <rect> elements in the paper');
assert.equal(textEls[0].textContent, V.sanitizeText('my rectangle'), 'text element has a proper content');
done();
}, this);
this.graph.resetCells([r1, r2, r3]);
});
QUnit.test('async: addCells', function(assert) {
this.paper.options.async = true;
this.paper.unfreeze();
var done = assert.async();
var r1 = new joint.shapes.basic.Rect({
position: { x: 20, y: 30 },
size: { width: 120, height: 80 },
attrs: { text: { text: 'my rectangle' }}
});
var r2 = r1.clone();
var r3 = r1.clone();
var r4 = r1.clone();
var r5 = r1.clone();
this.graph.addCells([r1, r2]);
this.paper.on('render:done', function() {
var textEls = this.paper.svg.getElementsByTagName('text');
var rectEls = this.paper.svg.getElementsByTagName('rect');
assert.equal(textEls.length, 5, 'there is exactly 5 <text> elements in the paper');
assert.equal(rectEls.length, 5, 'there is exactly 5 <rect> elements in the paper');
assert.equal(textEls[0].textContent, V.sanitizeText('my rectangle'), 'text element has a proper content');
done();
}, this);
this.graph.addCells([r3, r4, r5]);
});
QUnit.test('getBBox()', 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 view = this.paper.findViewByModel(myrect);
var bbox = view.getBBox();
assert.equal(bbox.x, 20, 'bbox.x is correct');
assert.equal(bbox.y, 30, 'bbox.y is correct');
assert.equal(bbox.width, 120, 'bbox.width is correct');
assert.equal(bbox.height, 80, 'bbox.height is correct');
myrect.attr('text', { ref: 'rect', 'ref-y': 100 });
bbox = view.getBBox({ useModelGeometry: false });
assert.ok(bbox.height > 80, 'Translating text outside the rect: bbox.width grew.');
assert.equal(bbox.x, 20, 'bbox.x is correct');
assert.equal(bbox.y, 30, 'bbox.y is correct');
assert.equal(bbox.width, 120, 'bbox.width is correct');
bbox = view.getBBox({ useModelGeometry: true });
assert.equal(bbox.x, 20, 'Using model geometry: bbox.x is correct');
assert.equal(bbox.y, 30, 'bbox.y is correct');
assert.equal(bbox.width, 120, 'bbox.width is correct');
assert.equal(bbox.height, 80, 'bbox.height is correct');
});
QUnit.test('z index', function(assert) {
var r1 = new joint.shapes.basic.Rect;
var r2 = new joint.shapes.basic.Rect;
var r3 = new joint.shapes.basic.Rect;
this.graph.addCell(r1);
this.graph.addCell(r2);
this.graph.addCell(r3);
assert.ok(r1.get('z') < r2.get('z'), 'z index of the first added cell is lower than that of the second one');
assert.ok(r2.get('z') < r3.get('z'), 'z index of the second added cell is lower than that of the third one');
// Test removing/adding new cells to cover https://github.com/clientIO/JointJS_plus/issues/21.
r1.remove();
var r4 = new joint.shapes.basic.Rect;
this.graph.addCell(r4);
assert.ok(r2.get('z') < r3.get('z'), 'z index of the second added cell is lower than that of the third one');
assert.ok(r3.get('z') < r4.get('z'), 'z index of the third added cell is lower than that of the fourth, newly added, one');
});
QUnit.test('position()', function(assert) {
var r1 = new joint.shapes.basic.Rect({
position: { x: 100, y: 100 },
size: { width: 120, height: 80 },
attrs: { text: { text: 'my rectangle' }}
});
var r2 = new joint.shapes.basic.Rect({
position: { x: 10, y: 10 },
size: { width: 30, height: 30 }
});
r1.addTo(this.graph);
assert.checkBbox(
this.paper,
r1, 100, 100, 120, 80,
'getter "position()" returns the elements position.'
);
r1.position(200, 200);
assert.checkBbox(
this.paper,
r1,
200, 200, 120, 80,
'setter "position(a, b)" should move element to the given position.'
);
// parentRelative option
assert.throws(function() {
r2.position(100, 100, { parentRelative: true });
}, 'getter throws an error if "parentRelative" option passed and the element is not part of any collection.');
assert.throws(function() {
r2.position({ parentRelative: true });
}, 'getter throws an error if "parentRelative" option passed and the element is not part of any collection.');
r2.addTo(this.graph);
assert.deepEqual(
r2.position({ parentRelative: true }),
r2.position(),
'getter with "parentRelative" option works in same way as getter without this option for an unembed element.'
);
r1.embed(r2);
r2.position(10, 10, { parentRelative: true });
assert.checkBbox(
this.paper,
r2,
210, 210, 30, 30,
'setter "position(a, b)" with "parentRelative" option should move element to the position relative to its parent.'
);
assert.equal(
r2.position({ parentRelative: true }).toString(),
'10@10',
'getter with "parentRelative" option returns position relative to the element parent.'
);
// deep options
r1.position(30, 30, { deep: true });
assert.equal(
r1.position().toString(),
'30@30',
'setter with deep option sets the correct position on the element'
);
assert.equal(
r2.position().toString(),
'40@40',
'and moves the child to keep the original distance from the element origin'
);
});
QUnit.test('translate()', 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);
myrect.translate(50);
assert.checkBbox(this.paper, myrect, 70, 30, 120, 80, 'translate(50) should translate by 50px in x direction only');
myrect.translate(0, 20);
assert.checkBbox(this.paper, myrect, 70, 50, 120, 80, 'translate(0, 20) should translate by 20px in y direction only');
myrect.translate(10, 10);
assert.checkBbox(this.paper, myrect, 80, 60, 120, 80, 'translate(10, 10) should translate by 10px in both x and y directions');
myrect.translate(-10, -10);
assert.checkBbox(this.paper, myrect, 70, 50, 120, 80, 'translate(-10, -10) should translate back by 10px in both x and y directions');
});
QUnit.test('translate() with restrictedArea option', function(assert) {
var rect = new joint.shapes.basic.Rect({
position: { x: 20, y: 30 },
size: { width: 120, height: 80 }
});
var embed = new joint.shapes.basic.Rect({
position: { x: 100, y: 70 },
size: { width: 120, height: 80 }
});
this.graph.addCell(rect);
rect.translate(1000, 0, { restrictedArea: { x: 0, y: 0, height: 1000, width: 150 }});
assert.equal(rect.prop('position/x'), 30, 'restrictedArea is respected when the element is translated to the left.');
rect.translate(0, 1000, { restrictedArea: { x: 0, y: 0, height: 150, width: 1000 }});
assert.equal(rect.prop('position/y'), 70, 'restrictedArea is respected when the element is translated to the bottom.');
rect.translate(-1000, 0, { restrictedArea: { x: 10, y: 0, height: 1000, width: 1000 }});
assert.equal(rect.prop('position/x'), 10, 'restrictedArea is respected when the element is translated to the right.');
rect.translate(0, -1000, { restrictedArea: { x: 0, y: 10, height: 1000, width: 1000 }});
assert.equal(rect.prop('position/y'), 10, 'restrictedArea is respected when the element is translated to the top.');
rect.position(50, 50).embed(embed);
this.graph.addCell(embed);
rect.translate(1000, 0, { restrictedArea: { x: 0, y: 0, height: 1000, width: 500 }});
assert.equal(rect.prop('position/x'), 330, 'restrictedArea is respected when the element and its embeds are translated to the left.');
rect.translate(0, 1000, { restrictedArea: { x: 0, y: 0, height: 500, width: 1000 }});
assert.equal(rect.prop('position/y'), 400, 'restrictedArea is respected when the element and its embeds are translated to the bottom.');
rect.position(50, 50);
embed.position(20, 20);
rect.translate(-1000, 0, { restrictedArea: { x: 10, y: 0, height: 1000, width: 1000 }});
assert.equal(rect.prop('position/x'), 40, 'restrictedArea is respected when the element and its embeds are translated to the right.');
rect.translate(0, -1000, { restrictedArea: { x: 0, y: 10, height: 1000, width: 1000 }});
assert.equal(rect.prop('position/y'), 40, 'restrictedArea is respected when the element and its embeds are translated to the top.');
});
QUnit.test('size()', function(assert) {
assert.expect(9);
var el = new joint.shapes.basic.Rect({
size: { width: 1, height: 2 }
});
// Getter
assert.deepEqual(el.size(), el.get('size'), 'work as getter');
assert.notOk(el.size() === el.get('size'), 'getter clones size');
// Setter
assert.equal(el.size(1, 2), el, 'chaining enabled');
assert.deepEqual(el.size(2, 3).size(), { width: 2, height: 3 }, 'set via width & height');
assert.deepEqual(el.size({ width: 3, height: 4 }).size(), { width: 3, height: 4 }, 'set via object');
assert.deepEqual(el.size({ width: 4 }).size(), { width: 4, height: 4 }, 'set via object with width only');
assert.deepEqual(el.size({ height: 5 }).size(), { width: 4, height: 5 }, 'set via object with height only');
// Setter with option
el.on('change:size', function(model, size, opt) {
assert.ok(opt.test);
});
el.size(10, 10, { test: true });
el.size({ width: 20, height: 20 }, { test: true });
});
QUnit.test('resize()', function(assert) {
var myrect = new joint.shapes.basic.Rect({
position: { x: 20, y: 30 },
size: { width: 120, height: 80 },
attrs: { text: { text: '' }}
});
this.graph.addCell(myrect);
myrect.resize(120, 80);
assert.checkBboxApproximately(1/* +- */, myrect.getBBox(), {
x: 20,
y: 30,
width: 120,
height: 80
}, 'resize([same width], [same height]) should not change bbox');
myrect.resize(240, 160);
assert.checkBboxApproximately(1/* +- */, myrect.getBBox(), {
x: 20,
y: 30,
width: 240,
height: 160
}, 'resize([2*width], [2*height]) should scale twice preserving origin as it was');
myrect.resize(120, 80);
assert.checkBboxApproximately(1/* +- */, myrect.getBBox(), {
x: 20,
y: 30,
width: 120,
height: 80
}, 'resize([orig width], [orig height]): should scale back to the original size and origin');
myrect.resize(200, 160, { direction: 'right' });
assert.checkBboxApproximately(1/* +- */, myrect.getBBox(), {
x: 20,
y: 30,
width: 200,
height: 80
}, 'resize([new width], [new height], { direction: "right" }) should scale only width, origin should be unchanged');
myrect.resize(80, 240, { direction: 'bottom' });
assert.checkBboxApproximately(1/* +- */, myrect.getBBox(), {
x: 20,
y: 30,
width: 200,
height: 240
}, 'resize([new width], [new height], { direction: "bottom" }) should scale only height, origin should be unchanged');
myrect.resize(50, 50, { direction: 'bottom-right' });
assert.checkBboxApproximately(1/* +- */, myrect.getBBox(), {
x: 20,
y: 30,
width: 50,
height: 50
}, 'resize([new width], [new height], { direction: "bottom-right" }) should scale both width and height, origin should be unchanged');
myrect.resize(20, 20, { direction: 'top-left' });
assert.checkBboxApproximately(1/* +- */, myrect.getBBox(), {
x: 50,
y: 60,
width: 20,
height: 20
}, 'resize([new width], [new height], { direction: "top-left" }) should scale both width and height, should change position');
myrect.resize(100, 100, { direction: 'left' });
assert.checkBboxApproximately(1/* +- */, myrect.getBBox(), {
x: -30,
y: 60,
width: 100,
height: 20
}, 'resize([new width], [new height], { direction: "left" }) should scale only width, should change position');
myrect.resize(30, 30, { direction: 'top' });
assert.checkBboxApproximately(1/* +- */, myrect.getBBox(), {
x: -30,
y: 50,
width: 100,
height: 30
}, 'resize([new width], [new height], { direction: "top" }) should scale only height, should change position');
myrect.resize(200, 200, { direction: 'top-right' });
assert.checkBboxApproximately(1/* +- */, myrect.getBBox(), {
x: -30,
y: -120,
width: 200,
height: 200
}, 'resize([new width], [new height], { direction: "top-right" }) should scale both width and height, should change position');
myrect.resize(10, 10, { direction: 'bottom-left' });
assert.checkBboxApproximately(1/* +- */, myrect.getBBox(), {
x: 160,
y: -120,
width: 10,
height: 10
}, 'resize([new width], [new height], { direction: "bottom-left" }) should scale both width and height, should change position');
});
QUnit.test('rotate()', 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);
myrect.rotate(90);
assert.checkBbox(this.paper, myrect, 40, 10, 80, 120, 'rotate(90) should rotate the object by 90 degrees around its center');
myrect.rotate(-90);
assert.checkBbox(this.paper, myrect, 20, 30, 120, 80, 'rotate(-90) should rotate the object back to its original angle');
// Rotation around an origin.
myrect.rotate(180, false, { x: 140, y: 70 });
assert.checkBbox(this.paper, myrect, 140, 30, 120, 80, 'rotate(180, 140, 70) should rotate the object around the middle of its right edge');
myrect.rotate(180, true, { x: 140, y: 70 });
assert.checkBbox(this.paper, myrect, 140, 30, 120, 80, 'rotate(180, 140, 70) with absolute flag should not rotate the object as it is already rotated');
});
QUnit.test('object reconstruction after several transformations', function(assert) {
var r1 = new joint.shapes.basic.Rect({
size: { width: 120, height: 80 },
attrs: { text: { text: 'my rectangle' }}
});
this.graph.addCell(r1);
r1.resize(150, 60);
r1.rotate(45);
r1.translate(20, 30);
r1.rotate(15);
r1.resize(20, 30);
r1.rotate(45, true);
r1.rotate(-15);
var r2 = r1.clone();
this.graph.addCell(r2);
var r1View = this.paper.findViewByModel(r1);
var r2View = this.paper.findViewByModel(r2);
var r1Bbox = g.rect(r1View.getBBox()).round();
var r2Bbox = g.rect(r2View.getBBox()).round();
assert.deepEqual(
{ x: r1Bbox.x, y: r1Bbox.y, width: r1Bbox.width, height: r1Bbox.height },
{ x: r2Bbox.x, y: r2Bbox.y, width: r2Bbox.width, height: r2Bbox.height },
'bounding box of the clone of an element that has been severely transformed is the same as of the original element'
);
});
QUnit.test('attr()', function(assert) {
var el = new joint.shapes.basic.Generic({
position: { x: 20, y: 30 },
markup: '<rect class="big"/><rect class="small"/>',
attrs: {
'.big': { width: 100, height: 50, fill: 'gray' },
'.small': { width: 10, height: 10, fill: 'red' }
}
});
this.graph.addCell(el);
var elView = this.paper.findViewByModel(el);
assert.equal(elView.$('.big').attr('opacity'), undefined, 'No opacity is set on the element');
el.attr({ '.big': { opacity: .5 }});
assert.equal(elView.$('.big').attr('opacity'), .5, '.5 opacity was correctly set by attr()');
assert.equal(el.attr(), el.get('attrs'), 'called with no arguments returns all `attrs`');
});
QUnit.test('removeAttr()', function(assert) {
var el = new joint.shapes.basic.Generic({
position: { x: 20, y: 30 },
markup: '<rect class="big"/><rect class="small"/>',
attrs: {
'.big': { width: 100, height: 50, fill: 'gray', stroke: 'pink' },
'.small': { width: 10, height: 10, fill: 'red' }
}
});
this.graph.addCell(el);
var elView = this.paper.findViewByModel(el);
assert.equal(elView.$('.big').attr('stroke'), 'pink', 'A stroke is set on the element');
el.removeAttr('.big/stroke');
assert.equal(elView.$('.big').attr('stroke'), undefined, 'The stroke was correctly unset from the element by removeAttr()');
var link = new joint.dia.Link({
source: { x: 100, y: 100 },
target: { x: 200, y: 200 },
attrs: {
'.connection': { width: 100, height: 50, fill: 'gray', 'stroke-width': 2 }
}
});
this.graph.addCell(link);
var linkView = this.paper.findViewByModel(link);
assert.equal(linkView.$('.connection').attr('stroke-width'), '2', 'A stroke is set on the link');
link.removeAttr('.connection/stroke-width');
assert.equal(linkView.$('.connection').attr('stroke-width'), undefined, 'The stroke was correctly unset from the link by removeAttr()');
});
QUnit.test('prop()', function(assert) {
var el = new joint.shapes.basic.Rect({
flat: 5,
object: { nested: { value: 'foo' }, nested2: { value: 'bar' }},
array: [[5], [{ value: ['bar'] }]],
a: { b: { c: 1 }}
});
assert.equal(el.prop('flat'), 5, 'flat value returned in getter');
assert.equal(el.prop('object/nested/value'), 'foo', 'nested object value returned in getter');
assert.deepEqual(el.prop('array/0'), [5], 'nested array returned in getter');
assert.equal(el.prop('array/0/0'), 5, 'value in nested array returned in getter');
assert.deepEqual(el.prop('array/1/0/value'), ['bar'], 'object in nested array returned in getter');
assert.equal(el.prop('array/1/0/value/0'), 'bar', 'value in nested object in nested array returned in getter');
el.prop('array/1/0/value/0', 'baz');
assert.equal(el.prop('array/1/0/value/0'), 'baz', 'value in nested object in nested array set correctly');
assert.ok(_.isArray(el.prop('array/1/0/value')), 'type of the nested array was preserved');
assert.ok(_.isObject(el.prop('array/1/0')), 'type of the nested object was preserved');
assert.ok(_.isArray(el.prop('array/1')), 'type of the nested array was preserved');
assert.ok(_.isArray(el.prop('array')), 'type of the top level array was preserved');
el.prop('array/1/0/value', { s: 'baz' });
assert.deepEqual(el.prop('array/1/0/value'), { s: 'baz' }, 'value in nested object in nested array set correctly');
assert.ok(_.isObject(el.prop('array/1/0/value')), 'type of the object was changed');
el.prop('array/2', 10);
assert.ok(_.isArray(el.prop('array')), 'type of the top level array was preserved after adding new item');
assert.equal(el.prop('array/2'), '10', 'value of the newly added array item is correct');
el.prop({ array: [['foo']] });
assert.ok(_.isArray(el.prop('array')), 'type of the top level array was preserved after changing an item');
assert.equal(el.prop('array/0/0'), 'foo', 'value of the newly added array item is correct');
assert.ok(_.isArray(el.prop('array/0')), 'type of the nested array is correct');
var called = false;
el.once('change:array', function(cell, changed, opt) {
assert.ok(opt.flag, 'options object was correctly passed in path syntax of prop');
called = true;
});
el.prop('array/0', 'something', { flag: true });
assert.ok(called, 'on change callback with options passed was called');
called = false;
el.once('change:array', function(cell, changed, opt) {
assert.ok(opt.flag, 'options object was correctly passed in object syntax of prop');
called = true;
});
el.prop({ array: ['something else'] }, { flag: true });
assert.ok(called, 'on change callback with options passed was called');
el.prop('object/nested', 'baz');
assert.deepEqual(el.prop('object/nested2'), { value: 'bar' }, 'value in untouched nested object was preserved');
assert.equal(el.prop('object/nested'), 'baz', 'value in nested object was changed');
el.prop('a/b', { d: 2 }, { rewrite: true });
assert.deepEqual(el.prop('a/b'), { d: 2 }, 'rewrite mode doesn\'t merge values');
});
QUnit.test('removeProp()', function(assert) {
assert.expect(4);
var el = new joint.dia.Cell({
flat: 6,
nested: { a: 4, b: 5 }
});
el.removeProp('NonExisting');
assert.deepEqual(el.attributes, {
id: el.id,
flat: 6,
nested: { a: 4, b: 5 }
}, 'Removing a non-existing property won\'t affect the model\'s attributes.');
el.removeProp('flat');
assert.ok(!el.has('flat'), 'A flat property was unset from the model.');
el.removeProp('nested/a');
assert.deepEqual(el.get('nested'), { b: 5 }, 'A nested property was unset from the model.');
el.on('change', function(cell, opt) {
assert.ok(opt.OPT_PRESENT, 'Options are propagated to the underlying model method.');
});
el.removeProp('nested/b', { OPT_PRESENT: true });
});
QUnit.test('removeProp()', function(assert) {
var el = new joint.dia.Cell({
flat: [1, 2, 3],
nested: { a: [1, 2, 3] }
});
el.removeProp('flat/2');
assert.deepEqual(el.get('flat'), [1, 2, undefined]);
el.removeProp('nested/a/2');
assert.deepEqual(el.get('nested'), { a: [1, 2, undefined] });
});
QUnit.test('toBack(), toFront()', function(assert) {
var r1 = new joint.shapes.basic.Rect;
var r2 = new joint.shapes.basic.Rect;
this.graph.addCell(r1);
this.graph.addCell(r2);
var r1View = this.paper.findViewByModel(r1);
var r2View = this.paper.findViewByModel(r2);
assert.notEqual(r2View.$el.prevAll(r1View.$el).length, 0, 'r1 element is before r2 element in the DOM');
r1.toFront();
assert.equal(r2View.$el.prevAll(r1View.$el).length, 0, 'r1 element moved after r2 element in the DOM after toFront()');
r1.toBack();
assert.notEqual(r2View.$el.prevAll(r1View.$el).length, 0, 'r1 element moved back before r2 element in the DOM after toBack()');
// Tests do not pass in Phantom JS
// r1.set('z', 10);
// r2.set('z', 10);
// r1.toFront();
// assert.equal(r1View.$el.index(), 1, 'r1 - front');
// r1.set('z', 10);
// r2.set('z', 10);
// r1.toBack();
// assert.equal(r1View.$el.index(), 0, 'r1 - back');
// r1.set('z', 10);
// r2.set('z', 10);
// r2.toFront();
// assert.equal(r2View.$el.index(), 1, 'r2 - front');
// r1.set('z', 10);
// r2.set('z', 10);
// r2.toBack();
// assert.equal(r2View.$el.index(), 0, 'r2 - back');
});
QUnit.test('toBack(), toFront() ignorable', function(assert) {
var r1 = new joint.shapes.basic.Rect;
var r2 = new joint.shapes.basic.Rect;
this.graph.addCell(r1);
this.graph.addCell(r2);
var r1Z = r1.get('z');
var r2Z = r2.get('z');
assert.ok(r1Z < r2Z, 'r1 z is lower than r2 z');
r2.toFront();
assert.equal(r2.get('z'), r2Z, 'r2 doesn\'t change z during a toFront() if it is already in place');
r1.toBack();
assert.equal(r1.get('z'), r1Z, 'r1 doesn\'t change z during a toBack() if it is already in place');
});
QUnit.test('toBack(), toFront() with active batch', function(assert) {
var r1 = new joint.shapes.basic.Rect;
var r2 = new joint.shapes.basic.Rect;
this.graph.addCell(r1);
this.graph.addCell(r2);
var spy = sinon.spy(this.paper, 'sortViews');
var r1View = this.paper.findViewByModel(r1);
var r2View = this.paper.findViewByModel(r2);
assert.notEqual(r2View.$el.prevAll(r1View.$el).length, 0, 'r1 element is before r2 element in the DOM');
r1.startBatch('to-front');
r1.toFront();
assert.equal(spy.callCount, 0, 'paper not sorted');
assert.notEqual(r2View.$el.prevAll(r1View.$el).length, 0, 'r1 element not moved after r2 element in the DOM after toFront() bacause a batch is running');
r1.stopBatch('to-front');
assert.equal(spy.callCount, 1, 'paper sorted exactly once');
assert.equal(r2View.$el.prevAll(r1View.$el).length, 0, 'r1 element moved after r2 element in the DOM after stopBatch()');
r1.startBatch('to-back');
r1.toBack();
assert.equal(spy.callCount, 1, 'paper not sorted');
assert.equal(r2View.$el.prevAll(r1View.$el).length, 0, 'r1 element not moved back before r2 element in the DOM after toBack() because a batch is running');
r1.stopBatch('to-back');
assert.equal(spy.callCount, 2, 'paper sorted exactly once');
assert.notEqual(r2View.$el.prevAll(r1View.$el).length, 0, 'r1 element moved back before r2 element in the DOM after stopBatch()');
});
QUnit.test('toBack(), toFront() with { deep: true } option', function(assert) {
var a1 = new joint.shapes.basic.Rect;
var a2 = new joint.shapes.basic.Rect;
var a3 = new joint.shapes.basic.Rect;
var a4 = new joint.shapes.basic.Rect;
a1.embed(a2).embed(a3.embed(a4));
var b1 = new joint.shapes.basic.Rect;
var b2 = new joint.shapes.basic.Rect;
this.graph.addCells([b1, a1, a2, a3, a4, b2]);
var a1View = this.paper.findViewByModel(a1);
var a2View = this.paper.findViewByModel(a2);
var a3View = this.paper.findViewByModel(a3);
var a4View = this.paper.findViewByModel(a4);
var b1View = this.paper.findViewByModel(b1);
var b2View = this.paper.findViewByModel(b2);
assert.equal(b2View.$el.nextAll('[data-type="basic.Rect"]').length, 0, 'element b2 after a1 element in the DOM');
assert.equal(b1View.$el.prevAll('[data-type="basic.Rect"]').length, 0, 'element b1 before a1 element in the DOM');
a1.toFront({ deep: true });
assert.equal(_.uniq(a1View.$el.prevAll('[data-type="basic.Rect"]').toArray().concat([b1View.el, b2View.el])).length, 2, 'a1 element moved after b1, b2 element in the DOM after toFront()');
assert.ok(a4View.$el.prev('[data-type="basic.Rect"]')[0] == a3View.el || a4View.$el.prev('[data-type="basic.Rect"]')[0] == a2View.el, 'and a4 element moved after a3 or a2 element');
assert.ok(a2View.$el.prev('[data-type="basic.Rect"]')[0] == a1View.el || a3View.$el.prev('[data-type="basic.Rect"]')[0] == a1View.el, 'and a2 or a3 element moved just after a1 element');
a1.toBack({ deep: true });
assert.equal(a1View.$el.prevAll('[data-type="basic.Rect"]').length, 0, 'a1 element moved back before a2, a3, a4, b1, b2 elements in the DOM after toBack()');
assert.ok(a4View.$el.prev('[data-type="basic.Rect"]')[0] == a3View.el || a4View.$el.prev('[data-type="basic.Rect"]')[0] == a2View.el, 'and a4 element moved after a3 or a2 element');
assert.ok(a2View.$el.prev('[data-type="basic.Rect"]')[0] == a1View.el || a3View.$el.prev('[data-type="basic.Rect"]')[0] == a1View.el, 'and a2 or a3 element moved just after a1 element');
});
QUnit.test('toBack(), toFront() ignore with { deep: true } option', function(assert) {
var a1 = new joint.shapes.basic.Rect;
var a2 = new joint.shapes.basic.Rect;
var a3 = new joint.shapes.basic.Rect;
var a4 = new joint.shapes.basic.Rect;
a1.embed(a2).embed(a3.embed(a4));
var b1 = new joint.shapes.basic.Rect;
var b2 = new joint.shapes.basic.Rect;
this.graph.addCells([b1, b2, a1, a2, a3, a4]);
var a1Z = a1.get('z');
var b1Z = b1.get('z');
assert.ok(b1Z < a1Z, 'b root z is lower than a root z');
a1.toFront({ deep: true });
assert.equal(a1.get('z'), a1Z, 'a1 doesn\'t change z during a toFront() if it is already in place');
b1.toBack({ deep: true });
assert.equal(b1.get('z'), b1Z, 'b1 doesn\'t change z during a toFront() if it is already in place');
});
QUnit.module('toBack(), toFront()', function(hooks) {
var cells, el;
var Rect = joint.shapes.standard.Rectangle;
hooks.beforeEach(function() {
var a1 = new Rect({ id: 'a1' });
var a11 = new Rect({ id: 'a11' });
var a111 = new Rect({ id: 'a111' });
var a112 = new Rect({ id: 'a112' });
var a12 = new Rect({ id: 'a12' });
var a121 = new Rect({ id: 'a121' });
var a122 = new Rect({ id: 'a122' });
var b1 = new Rect({ id: 'b1' });
var c1 = new Rect({ id: 'c1' });
cells = [b1, c1, a1, a11, a111, a112, a12, a121, a122];
cells.forEach(function(cell) { return cell.set('z', 0); });
a1.embed(a11.embed(a111.embed(a112)));
a1.embed(a12.embed(a121.embed(a122)));
this.graph.addCells(cells);
el = a1;
});
[{
// Test Case { deep: true, breadthFirst: false }
breadthFirst: false,
toFront: [0,0,1,2,4,5,3,6,7],
toBack: [0,0,-7,-6,-4,-3,-5,-2,-1]
}, {
// Test Case { deep: true, breadthFirst: true }
breadthFirst: true,
toFront: [0,0,1,2,4,6,3,5,7],
toBack: [0,0,-7,-6,-4,-2,-5,-3,-1]
}].forEach(function(testCase) {
QUnit.test('toBack(), toFront() > breadthFirst = ' + testCase.breadthFirst, function(assert) {
Array.from({ length: 2 }).forEach(function() {
el.toFront({ deep: true, breadthFirst: testCase.breadthFirst });
assert.deepEqual(
cells.map(function(cell) { return cell.get('z'); }),
testCase.toFront,
'toFront order'
);
});
Array.from({ length: 2 }).forEach(function() {
el.toBack({ deep: true, breadthFirst: testCase.breadthFirst });
assert.deepEqual(
cells.map(function(cell) { return cell.get('z'); }),
testCase.toBack,
'toBack order'
);
});
});
});
});
QUnit.test('fitEmbeds()', function(assert) {
var mainGroup = new joint.shapes.basic.Rect;
var group1 = new joint.shapes.basic.Rect({ position: { x: 0, y: 0 }, size: { width: 10, height: 10 }});
var group2 = new joint.shapes.basic.Rect({ position: { x: 1000, y: 1000 }, size: { width: 10, height: 10 }});
var a = new joint.shapes.basic.Rect({ position: { x: 100, y: 100 }, size: { width: 20, height: 20 }});
var b = new joint.shapes.basic.Rect({ position: { x: 200, y: 100 }, size: { width: 20, height: 20 }});
var c = new joint.shapes.basic.Rect({ position: { x: 150, y: 200 }, size: { width: 20, height: 20 }});
mainGroup.embed(group2.embed(c)).embed(group1.embed(a).embed(b));
assert.throws(function() {
a.fitEmbeds({ deep: true });
}, /graph/, 'Calling method on element that is not part of a graph throws an error.');
this.graph.addCells([mainGroup, group1, group2, a, b, c]);
a.fitEmbeds();
assert.deepEqual(a.getBBox(), g.rect(100, 100, 20, 20), 'Calling method on element that has no embeds has no effect.');
mainGroup.fitEmbeds({ deep: false });
assert.deepEqual(mainGroup.getBBox(), g.rect(0, 0, 1010, 1010), 'Shallow call takes embeds only one level deep into account.');
mainGroup.fitEmbeds({ deep: true });
assert.deepEqual(mainGroup.getBBox(), g.rect(100, 100, 120, 120), 'Deep call takes all descendant embeds into account.');
assert.deepEqual(group1.getBBox(), g.rect(100, 100, 120, 20), 'After the call the first group fits its embeds.');
assert.deepEqual(group2.getBBox(), g.rect(150, 200, 20, 20), 'So the second group.');
mainGroup.fitEmbeds({ deep: true, padding: 10 });
assert.deepEqual(mainGroup.getBBox(), g.rect(80, 80, 160, 160), 'Using padding options is expanding the groups.');
});
QUnit.test('clone()', function(assert) {
var r1 = new joint.shapes.basic.Rect({
position: { x: 20, y: 30 },
size: { width: 120, height: 80 },
attrs: { text: { text: 'my rectangle' }}
});
this.graph.addCell(r1);
var r2 = r1.clone();
this.graph.addCell(r2);
var textEls = this.paper.svg.getElementsByTagName('text');
var rectEls = this.paper.svg.getElementsByTagName('rect');
assert.equal(textEls.length, 2, 'there are exactly two <text> elements in the paper');
assert.equal(rectEls.length, 2, 'there are exactly two <rect> elements in the paper');
assert.equal(textEls[0].textContent, V.sanitizeText('my rectangle'), 'text element has a proper content');
assert.equal(textEls[1].textContent, V.sanitizeText('my rectangle'), 'text element of the cloned element has a proper content');
assert.checkBbox(this.paper, r2, 20, 30, 120, 80, 'cloned element is at the exact same position as the original element');
// Check correct offset of the element when translate() is called before appending the element to the paper.
// This is critical as in this situation, render() is called after translate() and should therefore
// reset the transformation attribute of the element.
var r3 = r1.clone();
r3.translate(50);
this.graph.addCell(r3);
assert.checkBbox(this.paper, r3, 70, 30, 120, 80, 'cloned element is offset by 50px to the right of the original element if translate() was called before appending it to the paper');
// Shallow clone of embedded elements
r1.embed(r2);
r2.embed(r3);
var clone = r2.clone();
assert.notOk(
clone.get('parent'),
'Shallow clone of embedded element has no parent.'
);
assert.ok(
_.isEmpty(clone.get('embeds')),
'Shallow clone of embedded element that is also a parent has no embeds.'
);
// Deep clone.
var l = new joint.dia.Link({ source: { id: r1.id }, target: { id: r2.id }});
this.graph.addCell(l);
var clones = r1.clone({ deep: true });
assert.equal(clones.length, 3, 'deep clone returned two clones for a parent element with one child not including the link (use graph.cloneSubgraph() if this is desired)');
assert.ok((clones[0].id === clones[1].get('parent') || (clones[1].id === clones[0].get('parent'))), 'clone of the embedded element gets a parent attribute set to the clone of the parent element');
this.graph.clear();
this.setupTestNestedGraph(this.graph);
clones = this.graph.getCell('a').clone({ deep: true });
assert.deepEqual(_.map(clones, function(c) {
return c.get('name');
}), ['a', 'aa', 'c', 'l2', 'aaa'], 'clone({ deep: true }) returns clones including all embedded cells');
});
QUnit.test('embed(), unembed()', function(assert) {
var r1 = new joint.shapes.basic.Rect({
position: { x: 20, y: 30 },
size: { width: 120, height: 80 },
attrs: { text: { text: 'my rectangle' }}
});
this.graph.addCell(r1);
var r2 = r1.clone();
this.graph.addCell(r2);
r1.embed(r2);
r1.translate(50);
assert.checkBbox(this.paper, r1, 70, 30, 120, 80, 'translate(50) should translate the parent element by 50px');
assert.checkBbox(this.paper, r2, 70, 30, 120, 80, 'embedded element should translate the same as the parent element');
assert.equal(r2.get('parent'), r1.id, 'embedded element gains the parent attribute pointing to its parent cell');
r1.unembed(r2);
r1.translate(-50);
assert.checkBbox(this.paper, r1, 20, 30, 120, 80, 'translate(-50) should translate the parent element by -50px');
assert.checkBbox(this.paper, r2, 70, 30, 120, 80, 'unembedded element should stay at the same position when its old parent got translated');
assert.equal(r2.get('parent'), undefined, 'embedded element gets its parent attribute pointing to its parent cell removed');
r1.embed(r2);
r2.remove();
assert.deepEqual(r1.get('embeds'), [], 'embedded element got removed from the embeds array of its parent when the embedded element remove() was called.');
});
QUnit.test('isEmbeddedIn()', function(assert) {
var r1 = new joint.shapes.basic.Rect;
var r2 = r1.clone();
var r3 = r1.clone();
r1.embed(r2);
r2.embed(r3);
this.graph.addCells([r1, r2, r3]);
assert.ok(!r1.isEmbeddedIn(r1), 'We have 3 elements. r3 is embedded in r2, r2 is embedded in r1. | r1 is not child of r1. ');
assert.ok(r2.isEmbeddedIn(r1), 'r2 is descendent of r1');
assert.ok(r3.isEmbeddedIn(r1), 'r3 is descendent of r1');
assert.ok(r3.isEmbeddedIn(r1, { deep: false }), 'r3 is not direct child of r1 (option { deep: false })');
assert.ok(!r1.isEmbeddedIn(r3), 'r1 is not descendent of r3');
});
QUnit.test('findMagnet()', function(assert) {
var r1 = new joint.shapes.basic.Rect({
attrs: { text: { text: 'my\nrectangle' }}
});
this.graph.addCell(r1);
var r1View = this.paper.findViewByModel(r1);
var magnet = r1View.findMagnet('tspan');
assert.equal(magnet, r1View.el, 'should return the root element of the view if there is no subelement with magnet attribute set to true');
r1.attr({ text: { magnet: true }});
magnet = r1View.findMagnet('tspan');
assert.equal(magnet, r1View.$('text')[0], 'should return the text element that has the magnet attribute set to true even though we passed the child <tspan> in the selector');
r1.attr({
text: { magnet: false },
'.': { magnet: false }
});
magnet = r1View.findMagnet('tspan');
assert.equal(magnet, undefined, 'should return `undefined` when magnet set to false on both the text and the root element');
});
QUnit.test('getSelector()', function(assert) {
var model = new joint.shapes.devs.Model({ inPorts: ['1', '2'], outPorts: ['3', '4'] });
// See issue #130 (https://github.com/DavidDurman/joint/issues/130)
this.graph.addCell([model.clone(), model.clone(), model.clone(), model]);
var view = model.findView(this.paper);
var svgText = view.el.querySelector('.label');
var svgPort1 = view.el.querySelector('[port="1"]');
var svgPort2 = view.el.querySelector('[port="2"]');
var svgPort3 = view.el.querySelector('[port="3"]');
var svgPort4 = view.el.querySelector('[port="4"]');
var selector;
selector = view.getSelector(svgText);
assert.equal(view.$el.find(selector)[0], svgText, 'Applying the selector returned from getSelector() should point to the selected element. It finds the exact same text node.');
selector = view.getSelector(svgPort1);
assert.equal(view.$el.find(selector)[0], svgPort1, 'It finds the exact same port no. 1.');
selector = view.getSelector(svgPort2);
assert.equal(view.$el.find(selector)[0], svgPort2, 'It finds the exact same port no. 2.');
selector = view.getSelector(svgPort3);
assert.equal(view.$el.find(selector)[0], svgPort3, 'It finds the exact same port no. 3.');
selector = view.getSelector(svgPort4);
assert.equal(view.$el.find(selector)[0], svgPort4, 'It finds the exact same port no. 4.');
});
QUnit.test('ports', function(assert) {
var model = new joint.shapes.devs.Model({
position: {
x: 40,
y: 40
},
size: {
width: 80,
height: 60
},
inPorts: ['1', '2'],
outPorts: ['3', '4']
});
this.graph.addCell(model);
var view = this.paper.findViewByModel(model);
var allPorts = model.get('inPorts').concat(model.get('outPorts'));
_.each(allPorts, function(port) {
var $portEl = view.$el.find('[port="' + port + '"]');
var foundEl = $portEl.length > 0;
assert.equal(foundEl, true, 'port DOM element should exist ("' + port + '")');
});
model.set('inPorts', ['1']);
model.set('outPorts', ['4']);
var removedPorts = ['2', '3'];
_.each(allPorts, function(port) {
var $portEl = view.$el.find('[port="' + port + '"]');
var foundEl = $portEl.length > 0;
var wasRemoved = _.indexOf(removedPorts, port) !== -1;
if (wasRemoved) {
assert.equal(foundEl, false, 'port DOM element should not exist ("' + port + '")');
} else {
assert.equal(foundEl, true, 'port DOM element should exist ("' + port + '")');
}
});
});
QUnit.test('ref-x, ref-y, ref', function(assert) {
var el = new joint.shapes.basic.Generic({
markup: '<rect class="big"/><rect class="small"/><rect class="smaller"/>',
size: { width: 100, height: 50 },
attrs: {
'.big': { width: 100, height: 50, fill: 'gray' },
'.small': { width: 10, height: 10, 'ref-x': 20, 'ref-y': 10, fill: 'red' },
'.smaller': { width: 5, height: 5, 'ref-x': 20, 'ref-y': 10, ref: '.small', fill: 'black' }
}
});
this.graph.addCell(el);
var elView = this.paper.findViewByModel(el);
// Range [1, x]
var smallRectBbox = V(elView.$('.small')[0]).bbox(false, elView.el);
assert.deepEqual(
{ x: smallRectBbox.x, y: smallRectBbox.y, width: smallRectBbox.width, height: smallRectBbox.height },
{ x: 20, y: 10, width: 10, height: 10 },
'ref-x: 20, ref-y: 10 attributes should offset the element by 20px in x axis and 10px in y axis'
);
// Range [0, 1]
el.attr({ '.small': { 'ref-x': .5, 'ref-y': .5 }});
smallRectBbox = V(elView.$('.small')[0]).bbox(false, elView.el);
assert.deepEqual(
{ x: smallRectBbox.x, y: smallRectBbox.y, width: smallRectBbox.width, height: smallRectBbox.height },
{ x: 50, y: 25, width: 10, height: 10 },
'ref-x: .5, ref-y: .5 attributes should position the element in the center, i.e. at [50, 25] coordinate'
);
// Percentage
el.attr({ '.small': { 'ref-x': '50%', 'ref-y': '50%' }});
smallRectBbox = V(elView.$('.small')[0]).bbox(false, elView.el);
assert.deepEqual(
{ x: smallRectBbox.x, y: smallRectBbox.y, width: smallRectBbox.width, height: smallRectBbox.height },
{ x: 50, y: 25, width: 10, height: 10 },
'ref-x: "50%", ref-y: "50%" attributes should position the element in the center, i.e. at [50, 25] coordinate'
);
// Range [-x, 0]
el.attr({ '.small': { 'ref-x': -10, 'ref-y': -15 }});
smallRectBbox = V(elView.$('.small')[0]).bbox(false, elView.el);
assert.deepEqual(
{ x: smallRectBbox.x, y: smallRectBbox.y, width: smallRectBbox.width, height: smallRectBbox.height },
{ x: -10, y: -15, width: 10, height: 10 },
'ref-x: -10, ref-y: -15 attributes should offset the element from the left by 10px and from the top by 15px'
);
var smallerRectBbox = V(elView.$('.smaller')[0]).bbox(false, elView.el);
assert.deepEqual(
{
x: smallerRectBbox.x,
y: smallerRectBbox.y,
width: smallerRectBbox.width,
height: smallerRectBbox.height
},
{ x: smallRectBbox.x + 20, y: smallRectBbox.y + 10, width: 5, height: 5 },
'ref-x: 20, ref-y: 10 and ref set to .small should offset the element by 20px in x axis and 10px in y axis with respect to the x-y coordinate of the .small element'
);
assert.throws(function() {
el.attr({ '.small': { 'ref': '.not-existing-reference' }});
}, /dia.CellView/, 'Use of an invalid reference throws an error.');
});
QUnit.test('ref-dx, ref-dy, ref', function(assert) {
var el = new joint.shapes.basic.Generic({
markup: '<rect class="big"/><rect class="small"/><rect class="smaller"/>',
siz