node.setData({key1: 'value1', key2: 'value2'});
expect(node.getData()).to.eql({key1: 'value1', key2: 'value2'});
});
+
+ it('can remove specific data', function() {
+ node.setData('key', 'value');
+ node.setData('key', undefined);
+ expect(node.getData('key')).to.be.undefined;
+ });
});
describe('Changing node tag', function() {
it('can change tag name', function() {
var node = elementNodeFromXML('<div></div>');
- node.setTag('span');
+ node = node.setTag('span');
expect(node.getTagName()).to.equal('span');
});
- it('emits nodeTagChange event', function() {
- var node = elementNodeFromXML('<div></div>'),
- spy = sinon.spy();
-
- node.document.on('change', spy);
- node.setTag('span');
- var event = spy.args[0][0];
-
- expect(event.type).to.equal('nodeTagChange');
- expect(event.meta.node.sameNode(node)).to.be.true;
- expect(event.meta.oldTagName).to.equal('div');
- });
-
describe('Implementation specific expectations', function() {
- // DOM specifies ElementNode tag as a read-only property, so
- // changing it in a seamless way is a little bit tricky. For this reason
- // the folowing expectations are required, despite the fact that they actually are
- // motivated by implemetation details.
-
- it('keeps node in the document', function() {
- var doc = getDocumentFromXML('<div><header></header></div>'),
- header = doc.root.contents()[0];
- header.setTag('span');
- expect(header.parent().sameNode(doc.root)).to.be.true;
- });
it('keeps custom data', function() {
var node = elementNodeFromXML('<div></div>');
node.setData('key', 'value');
- node.setTag('header');
+ node = node.setTag('header');
expect(node.getTagName()).to.equal('header');
expect(node.getData()).to.eql({key: 'value'});
expect(doc.root.getTagName()).to.equal('span');
});
- it('keeps contents', function() {
+ it('keeps node contents', function() {
var node = elementNodeFromXML('<div><div></div></div>');
- node.setTag('header');
+ node = node.setTag('header');
expect(node.contents()).to.have.length(1);
});
});
+ });
describe('Setting node attributes', function() {
it('can set node attribute', function() {
});
});
+ describe('Searching for the last child text node', function() {
+ [
+ '<div>xxx<div></div>last</div>',
+ '<div><div>last</div></div>',
+ '<div>xxx<div>last</div><div></div></div>'
+ ].forEach(function(xml, i) {
+ var example = 'example ' + i;
+ it('returns last child text node ' + example + ')', function() {
+ var doc = getDocumentFromXML(xml),
+ lastTextNode = doc.root.getLastTextNode();
+ expect(lastTextNode.getText()).to.equal('last', example);
+ });
+ });
});
});
expect(rootContents[0].getText()).to.equal('Alice a cat');
});
+ it('merges adjacent text nodes resulting from moving an element node in between', function() {
+ var doc = getDocumentFromXML('<div><a></a>Alice <span>has</span>a cat</div>'),
+ span = doc.root.contents()[2],
+ a = doc.root.contents()[0];
+
+ a.append(span);
+
+ var rootContents = doc.root.contents();
+ expect(rootContents).to.have.length(2, 'one child left');
+ expect(rootContents[1].getText()).to.equal('Alice a cat');
+ });
+
it('inserts node at index', function() {
var doc = getDocumentFromXML('<div><a></a><b></b><c></c></div>'),
b = doc.root.contents()[1];
expect(event.meta.node.sameNode(appended)).to.be.true;
});
- it('emits nodeMoved when appending aready existing node', function() {
+ it('emits nodeDetached/nodeAdded events with `move` flag when appending aready existing node', function() {
var node = elementNodeFromXML('<div><a></a><b></b></div>'),
a = node.contents()[0],
b = node.contents()[1],
node.document.on('change', spy);
var appended = a.append(b),
- event = spy.args[0][0];
+ detachedEvent = spy.args[0][0],
+ addedEvent = spy.args[1][0];
+
+ expect(spy.callCount).to.equal(2);
+ expect(detachedEvent.type).to.equal('nodeDetached');
+ expect(detachedEvent.meta.node.sameNode(appended)).to.be.true;
+ expect(detachedEvent.meta.move).to.equal(true, 'move flag set to true for nodeDetachedEvent');
+ expect(addedEvent.type).to.equal('nodeAdded');
+ expect(addedEvent.meta.node.sameNode(appended)).to.be.true;
+ expect(addedEvent.meta.move).to.equal(true, 'move flag set to true for nodeAddedEvent');
- expect(spy.callCount).to.equal(1);
- expect(event.type).to.equal('nodeMoved');
- expect(event.meta.node.sameNode(appended)).to.be.true;
- expect(node.document.root.sameNode(event.meta.parent)).to.equal(true, 'previous parent attached to event meta');
});
it('emits nodeAdded event when prepending new node', function() {
expect(event.meta.node.sameNode(prepended)).to.be.true;
});
- it('emits nodeMoved when prepending aready existing node', function() {
+ it('emits nodeDetached/nodeAdded events with `move` flag when prepending aready existing node', function() {
var node = elementNodeFromXML('<div><a></a><b></b></div>'),
a = node.contents()[0],
b = node.contents()[1],
spy = sinon.spy();
node.document.on('change', spy);
-
+
var prepended = a.prepend(b),
- event = spy.args[0][0];
- expect(spy.callCount).to.equal(1);
- expect(event.type).to.equal('nodeMoved');
- expect(event.meta.node.sameNode(prepended)).to.be.true;
+ detachedEvent = spy.args[0][0],
+ addedEvent = spy.args[1][0];
+
+ expect(spy.callCount).to.equal(2);
+ expect(detachedEvent.type).to.equal('nodeDetached');
+ expect(detachedEvent.meta.node.sameNode(prepended)).to.be.true;
+ expect(detachedEvent.meta.move).to.equal(true, 'move flag set to true for nodeDetachedEvent');
+ expect(addedEvent.type).to.equal('nodeAdded');
+ expect(addedEvent.meta.node.sameNode(prepended)).to.be.true;
+ expect(addedEvent.meta.move).to.equal(true, 'move flag set to true for nodeAddedEvent');
});
it('emits nodeAdded event when inserting node after another', function() {
expect(event.meta.node.sameNode(inserted)).to.be.true;
});
- it('emits nodeMoved when inserting aready existing node after another', function() {
+ it('emits nodeDetached/nodeAdded events with `move` flag when inserting aready existing node after another', function() {
var node = elementNodeFromXML('<div><a></a><b></b></div>'),
a = node.contents()[0],
b = node.contents()[1],
spy = sinon.spy();
node.document.on('change', spy);
var inserted = b.after(a),
- event = spy.args[0][0];
+ detachedEvent = spy.args[0][0],
+ addedEvent = spy.args[1][0];
- expect(spy.callCount).to.equal(1);
- expect(event.type).to.equal('nodeMoved');
- expect(event.meta.node.sameNode(inserted)).to.be.true;
+ expect(spy.callCount).to.equal(2);
+ expect(detachedEvent.type).to.equal('nodeDetached');
+ expect(detachedEvent.meta.node.sameNode(inserted)).to.be.true;
+ expect(detachedEvent.meta.move).to.equal(true, 'move flag set to true for nodeDetachedEvent');
+ expect(addedEvent.type).to.equal('nodeAdded');
+ expect(addedEvent.meta.node.sameNode(inserted)).to.be.true;
+ expect(addedEvent.meta.move).to.equal(true, 'move flag set to true for nodeAddedEvent');
});
it('emits nodeAdded event when inserting node before another', function() {
expect(event.meta.node.sameNode(inserted)).to.be.true;
});
- it('emits nodeAdded when inserting aready existing node before another', function() {
+ it('emits nodeDetached/nodeAdded events with `move` flag when inserting aready existing node before another', function() {
var node = elementNodeFromXML('<div><a></a><b></b></div>'),
a = node.contents()[0],
b = node.contents()[1],
spy = sinon.spy();
node.document.on('change', spy);
var inserted = a.before(b),
- event = spy.args[0][0];
+ detachedEvent = spy.args[0][0],
+ addedEvent = spy.args[1][0];
- expect(spy.callCount).to.equal(1);
- expect(event.type).to.equal('nodeMoved');
- expect(event.meta.node.sameNode(inserted)).to.be.true;
+ expect(spy.callCount).to.equal(2);
+ expect(detachedEvent.type).to.equal('nodeDetached');
+ expect(detachedEvent.meta.node.sameNode(inserted)).to.be.true;
+ expect(detachedEvent.meta.move).to.equal(true, 'move flag set to true for nodeDetachedEvent');
+ expect(addedEvent.type).to.equal('nodeAdded');
+ expect(addedEvent.meta.node.sameNode(inserted)).to.be.true;
+ expect(addedEvent.meta.move).to.equal(true, 'move flag set to true for nodeAddedEvent');
});
it('emits nodeDetached and nodeAdded when replacing root node with another', function() {
var transaction = doc.undoStack[0];
expect(transaction.metadata).to.equal(metadata);
});
+
+ it('can be rolled back', function() {
+ var doc = getDocumentFromXML('<root></root>');
+
+ doc.startTransaction();
+ doc.root.append({tagName: 'div'});
+ doc.rollbackTransaction();
+
+ expect(doc.undoStack.length).to.equal(0, 'nothing to undo');
+ expect(doc.root.contents().length).to.equal(0);
+ });
+
+ it('rollbacks and calls error handleor if error gets thrown', function() {
+ var doc = getDocumentFromXML('<root></root>'),
+ err = new Error(),
+ spy = sinon.spy();
+
+ doc.transaction(function() {
+ doc.root.append({tagName: 'div'});
+ throw err;
+ }, {error: spy});
+
+ expect(spy.args[0][0]).to.equal(err);
+ expect(doc.root.contents().length).to.equal(0);
+ expect(doc.undoStack.length).to.equal(0);
+ });
});
describe('Regression tests', function() {
doc.redo();
expect(doc.root.getAttr('t')).to.equal('1');
});
+ it('can perform undo of an operation performed after automatic transaction rollback', function() {
+ var doc = getDocumentFromXML('<section></section>'),
+ extension = {document: {transformations: {
+ throwingTransformation: function() { throw new Error(); }
+ }}};
+
+ doc.registerExtension(extension);
+
+ doc.throwingTransformation();
+
+ doc.transaction(function() {
+ doc.root.setAttr('x', '2');
+ });
+
+ expect(doc.undoStack.length).to.equal(1);
+ expect(doc.root.getAttr('x')).to.equal('2');
+
+ doc.undo();
+
+ expect(doc.undoStack.length).to.equal(0);
+ expect(doc.root.getAttr('x')).to.be.undefined;
+
+ });
});
});