X-Git-Url: https://git.mdrn.pl/fnpeditor.git/blobdiff_plain/729243168cee79db77626f00e5b0073f692aba2e..5145fc38a6bf044367f4d96197dd3d47e05b6266:/src/smartxml/smartxml.test.js?ds=inline diff --git a/src/smartxml/smartxml.test.js b/src/smartxml/smartxml.test.js index b152d26..524c976 100644 --- a/src/smartxml/smartxml.test.js +++ b/src/smartxml/smartxml.test.js @@ -69,6 +69,24 @@ describe('smartxml', function() { expect(node.contents()[0].getText()).to.equal('Alice'); expect(node.contents()[1].getTagName()).to.equal('b'); }); + + describe('Retrieving node by path', function() { + it('passes smoke tests', function() { + var doc = getDocumentFromXML('c'); + expect(doc.getNodeByPath([0]).sameNode(doc.root.contents()[0])).to.be.true; + expect(doc.getNodeByPath([0,0]).sameNode(doc.root.contents()[0].contents()[0])).to.be.true; + }); + it('treats empty path as a root path', function() { + var doc = getDocumentFromXML(''); + expect(doc.getNodeByPath([]).sameNode(doc.root)).to.be.true; + }); + it('returns undefined for non existing paths', function() { + var doc = getDocumentFromXML(''); + expect(doc.getNodeByPath([1])).to.be.undefined; + expect(doc.getNodeByPath([0,1])).to.be.undefined; + expect(doc.getNodeByPath([10,1])).to.be.undefined; + }); + }); }); describe('DocumentNode', function() { @@ -163,46 +181,28 @@ describe('smartxml', function() { 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('
'); - node.setTag('span'); + node = node.setTag('span'); expect(node.getTagName()).to.equal('span'); }); - it('emits nodeTagChange event', function() { - var node = elementNodeFromXML('
'), - 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('
'), - 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('
'); node.setData('key', 'value'); - node.setTag('header'); + node = node.setTag('header'); expect(node.getTagName()).to.equal('header'); expect(node.getData()).to.eql({key: 'value'}); @@ -214,12 +214,13 @@ describe('smartxml', function() { expect(doc.root.getTagName()).to.equal('span'); }); - it('keeps contents', function() { + it('keeps node contents', function() { var node = elementNodeFromXML('
'); - 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() { @@ -243,6 +244,42 @@ describe('smartxml', function() { }); }); + describe('Searching for the last child text node', function() { + [ + '
xxx
last
', + '
last
', + '
xxx
last
' + ].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); + }); + }); + }); + + describe('Putting nodes around', function() { + it('will not allow to put node before or after root node', function() { + var doc = getDocumentFromXML(''), + spy = sinon.spy(), + root = doc.root, + result; + + doc.on('change', spy); + + result = doc.root.before({tagName: 'test'}); + + expect(spy.callCount).to.equal(0); + expect(result).to.undefined; + + result = doc.root.after({tagName: 'test'}); + + expect(spy.callCount).to.equal(0); + expect(result).to.undefined; + + expect(doc.root.sameNode(root)); + }); }); }); @@ -426,6 +463,18 @@ describe('smartxml', function() { 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('
Alice hasa cat
'), + 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('
'), b = doc.root.contents()[1]; @@ -529,12 +578,33 @@ describe('smartxml', function() { }); }); - it('wraps element node with another element node', function() { + it('wraps root element node with another element node', function() { var node = elementNodeFromXML('
'), wrapper = elementNodeFromXML(''); node.wrapWith(wrapper); expect(node.parent().sameNode(wrapper)).to.be.true; + expect(node.document.root.sameNode(wrapper)).to.be.true; + }); + + it('wraps element node with another element node', function() { + var doc = getDocumentFromXML('
'), + div = doc.root.contents()[0]; + + var wrapper = div.wrapWith({tagName: 'wrapper'}); + expect(wrapper.sameNode(doc.root.contents()[0])).to.equal(true, '1'); + expect(div.parent().sameNode(wrapper)).to.equal(true, '2'); + expect(wrapper.contents()[0].sameNode(div)).to.equal(true, '3'); + }); + + it('wraps element outside of document tree', function() { + var doc = getDocumentFromXML('
'), + node = doc.createDocumentNode({tagName: 'node'}); + + node.wrapWith({tagName: 'wrapper'}); + expect(node.parent().getTagName()).to.equal('wrapper'); + expect(node.parent().contents()[0].sameNode(node)).to.be.true; + expect(doc.root.getTagName()).to.equal('section'); }); it('unwraps element node contents', function() { @@ -549,6 +619,24 @@ describe('smartxml', function() { expect(node.contents()[2].getText()).to.equal(' a cat!'); }); + it('removes parent-describing sibling nodes of unwrapped node', function() { + var doc = getDocumentFromXML('
'), + div = doc.root.contents()[0], + x = div.contents()[1]; + + doc.registerExtension({documentNode: {methods: { + object: { + describesParent: function() { + return this.getTagName() === 'x'; + } + } + }}}); + + div.unwrapContent(); + expect(doc.root.contents().length).to.equal(2); + expect(x.isInDocument()).to.be.false; + }); + it('unwrap single element node from its parent', function() { var doc = getDocumentFromXML('
'), div = doc.root, @@ -601,6 +689,32 @@ describe('smartxml', function() { expect(wrapperContents[1].contents().length).to.equal(1); expect(wrapperContents[1].contents()[0].getText()).to.equal('small'); }); + + it('keeps parent-describing nodes in place', function() { + var doc = getDocumentFromXML('Alice probably has a cat'), + root = doc.root, + x = root.contents()[1], + y = root.contents()[3]; + + doc.registerExtension({documentNode: {methods: { + object: { + describesParent: function() { + /* globals Node */ + return this.nodeType === Node.ELEMENT_NODE && this.getTagName() === 'x'; + } + } + }}}); + + root.wrapText({ + _with: {tagName: 'span', attrs: {'attr1': 'value1'}}, + offsetStart: 1, + offsetEnd: 4, + textNodeIdx: [0,4] + }); + + expect(x.parent().sameNode(root)).to.be.true; + expect(y.parent().getTagName()).to.equal('span'); + }); }); describe('Wrapping Nodes', function() { @@ -649,6 +763,31 @@ describe('smartxml', function() { expect(headerChildren[0].sameNode(div2)).to.equal(true, 'first node wrapped'); expect(headerChildren[1].sameNode(div3)).to.equal(true, 'second node wrapped'); }); + + it('keeps parent-describing nodes in place', function() { + var section = elementNodeFromXML('
Alice
a cat
'), + aliceText = section.contents()[0], + x = section.contents()[1], + lastDiv = section.contents()[2]; + + section.document.registerExtension({documentNode: {methods: { + object: { + describesParent: function() { + return this.nodeType === Node.ELEMENT_NODE && this.getTagName() === 'x'; + } + } + }}}); + + section.document.wrapNodes({ + node1: aliceText, + node2: lastDiv, + _with: {tagName: 'header'} + }); + + expect(x.parent().sameNode(section)).to.be.true; + expect(aliceText.parent().getTagName()).to.equal('header'); + expect(lastDiv.parent().getTagName()).to.equal('header'); + }); }); }); @@ -821,6 +960,28 @@ describe('smartxml', function() { expect(contents[1].contents().length).to.equal(1); expect(contents[1].contents()[0].getText()).to.equal('b'); }); + it('removes across elements - 6', function() { + var doc = getDocumentFromXML('
aaabbbccc
ddd
'); + doc.deleteText({ + from: { + node: getTextNode('aaa', doc), + offset: 1 + }, + to: { + node: getTextNode('ddd', doc), + offset: 1 + } + }, { + error: function(e) {throw e;} + }); + + var contents = doc.root.contents(); + expect(contents.length).to.equal(2); + expect(contents[0].contents().length).to.equal(1); + expect(contents[0].contents()[0].getText()).to.equal('a'); + expect(contents[1].contents().length).to.equal(1); + expect(contents[1].contents()[0].getText()).to.equal('dd'); + }); }); describe('Splitting text', function() { @@ -928,7 +1089,7 @@ describe('smartxml', function() { 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('
'), a = node.contents()[0], b = node.contents()[1], @@ -936,12 +1097,17 @@ describe('smartxml', function() { 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() { @@ -955,18 +1121,24 @@ describe('smartxml', 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('
'), 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() { @@ -980,18 +1152,23 @@ describe('smartxml', 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('
'), 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() { @@ -1005,18 +1182,23 @@ describe('smartxml', 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('
'), 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() { @@ -1611,6 +1793,50 @@ describe('smartxml', function() { expect(doc.root.getAttr('smart')).to.equal('1'); expect(doc.root.getAttr('unaware')).to.equal('1'); }); + + it('can have associated metadata', function() { + var doc = getDocumentFromXML('
'), + metadata = Object.create({}); + + doc.registerExtension({document: {transformations: { + test: function() { + this.trigger('change'); + } + }}}); + + doc.startTransaction(metadata); + doc.test(); + doc.endTransaction(); + + var transaction = doc.undoStack[0]; + expect(transaction.metadata).to.equal(metadata); + }); + + it('can be rolled back', function() { + var doc = getDocumentFromXML(''); + + 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(''), + 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() { @@ -1639,6 +1865,29 @@ describe('smartxml', 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('
'), + 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; + + }); }); });