X-Git-Url: https://git.mdrn.pl/fnpeditor.git/blobdiff_plain/72da90cac0feafb35cac9292105e24f2b5094b45..d5f4c1bb0dc3a9ff8845d209c75d00cc733b3360:/src/smartxml/smartxml.test.js
diff --git a/src/smartxml/smartxml.test.js b/src/smartxml/smartxml.test.js
index 180fa87..0883ba7 100644
--- a/src/smartxml/smartxml.test.js
+++ b/src/smartxml/smartxml.test.js
@@ -1,12 +1,13 @@
define([
'libs/chai',
'libs/sinon',
+ 'libs/underscore',
'./smartxml.js'
-], function(chai, sinon, smartxml) {
+], function(chai, sinon, _, smartxml) {
'use strict';
/*jshint expr:true */
-/* global describe, it, beforeEach */
+/* global describe, it, beforeEach, Node, DOMParser */
var expect = chai.expect;
@@ -59,6 +60,15 @@ describe('smartxml', function() {
expect(emptyTextNode.getText()).to.equal('', 'empty ok');
expect(nonEmptyTextNode.getText()).to.equal('alice', 'non empty ok');
});
+
+ it('creates nodes from xml strings', function() {
+ var doc = getDocumentFromXML('
'),
+ node = doc.createDocumentNode('Alice');
+ expect(node.getTagName()).to.equal('a');
+ expect(node.contents().length).to.equal(2);
+ expect(node.contents()[0].getText()).to.equal('Alice');
+ expect(node.contents()[1].getTagName()).to.equal('b');
+ });
});
describe('DocumentNode', function() {
@@ -76,6 +86,31 @@ describe('smartxml', function() {
});
});
+ it('can be cloned with its contents and its contents data', function() {
+ var doc = getDocumentFromXML(''),
+ root = doc.root,
+ div = root.contents()[0];
+
+ var ClonableObject = function(arg) {
+ this.arg = arg;
+ };
+ ClonableObject.prototype.clone = function() {
+ return new ClonableObject(this.arg);
+ };
+
+ div.setData('key', 'value');
+ div.setData('clonableObject', new ClonableObject('test'));
+
+ var rootClone = root.clone(),
+ divClone = rootClone.contents()[0],
+ stringClone = divClone.getData('key'),
+ objClone = divClone.getData('clonableObject');
+
+ expect(stringClone).to.equal('value');
+ expect(objClone.arg).to.equal('test', 'clonable object got copied');
+ expect(objClone !== div.getData('clonableObject')).to.be.equal(true, 'copy of the clonable object is a new object');
+ });
+
it('knows its path in the document tree', function() {
var doc = getDocumentFromXML('text'),
root = doc.root,
@@ -300,10 +335,77 @@ describe('smartxml', function() {
});
});
+ describe('Dividing text node into two with element node', function() {
+ it('can divide text node with element node, splitting text node into two', function() {
+ var doc = getDocumentFromXML('Alice has a cat
'),
+ text = doc.root.contents()[0];
+
+ var returned = text.divideWithElementNode({tagName: 'aside'}, {offset: 5}),
+ contents = doc.root.contents(),
+ lhsText = contents[0],
+ rhsText = contents[2];
+
+ expect(lhsText.getText()).to.equal('Alice');
+ expect(returned.sameNode(contents[1]));
+ expect(rhsText.getText()).to.equal(' has a cat');
+ });
+
+ it('treats dividing at the very end as appending after it', function() {
+ var doc = getDocumentFromXML('Alice has a cat
'),
+ text = doc.root.contents()[0];
+
+
+ var returned = text.divideWithElementNode({tagName: 'aside'}, {offset: 15}),
+ contents = doc.root.contents(),
+ textNode = contents[0],
+ elementNode = contents[1];
+
+ expect(contents.length).to.equal(2);
+ expect(textNode.getText()).to.equal('Alice has a cat');
+ expect(returned.sameNode(elementNode)).to.be.true;
+ expect(elementNode.getTagName()).to.equal('aside');
+ });
+
+ it('treats dividing at the very beginning as prepending before it', function() {
+ var doc = getDocumentFromXML('Alice has a cat
'),
+ text = doc.root.contents()[0];
+
+ var returned = text.divideWithElementNode({tagName: 'aside'}, {offset: 0}),
+ contents = doc.root.contents(),
+ textNode = contents[1],
+ elementNode = contents[0];
+
+ expect(contents.length).to.equal(2);
+ expect(textNode.getText()).to.equal('Alice has a cat');
+ expect(returned.sameNode(elementNode)).to.be.true;
+ expect(elementNode.getTagName()).to.equal('aside');
+ });
+ });
});
describe('Manipulations', function() {
+ describe('replacing node with another one', function() {
+ it('replaces node with another one', function() {
+ var doc = getDocumentFromXML(''),
+ a = doc.root.contents()[0];
+
+ var c = a.replaceWith({tagName: 'b', attrs: {b:'1'}});
+
+ expect(doc.root.contents()[0].sameNode(c));
+ expect(c.getTagName()).to.equal('b');
+ expect(c.getAttr('b')).to.equal('1');
+ });
+ it('can replace document root', function() {
+ var doc = getDocumentFromXML('');
+
+ var header = doc.root.replaceWith({tagName: 'header'});
+
+ expect(doc.root.sameNode(header)).to.be.true;
+ expect(doc.containsNode(header)).to.be.true;
+ });
+ });
+
it('merges adjacent text nodes resulting from detaching an element node in between', function() {
var doc = getDocumentFromXML('Alice hasa cat
'),
span = doc.root.contents()[1];
@@ -315,6 +417,27 @@ describe('smartxml', function() {
expect(rootContents[0].getText()).to.equal('Alice a cat');
});
+ it('inserts node at index', function() {
+ var doc = getDocumentFromXML(''),
+ b = doc.root.contents()[1];
+
+ var inserted = doc.root.insertAtIndex({tagName: 'test'}, 1);
+
+ expect(doc.root.contents()[1].sameNode(inserted)).to.equal(true, 'inserted node returned');
+ expect(b.getIndex()).to.equal(2, 'b node shifted right');
+ });
+
+ it('appends node when inserting node at index out of range', function() {
+ var doc = getDocumentFromXML('');
+
+ var test1 = doc.root.insertAtIndex({tagName: 'test1'}, 0),
+ test2 = doc.root.insertAtIndex({tagName: 'test1'}, 10);
+
+ expect(doc.root.contents()[0].sameNode(test1)).to.equal(true, 'inserting at index 0 of empty nodes appends node');
+ expect(doc.root.contents().length).to.equal(1, 'inserting at index out of range does nothing');
+ expect(test2).to.equal(undefined, 'inserting at index out of range returns undefined');
+ });
+
it('appends element node to another element node', function() {
var node1 = elementNodeFromParams({tag: 'div'}),
node2 = elementNodeFromParams({tag: 'a'}),
@@ -417,8 +540,8 @@ describe('smartxml', function() {
lastDiv = section.contents()[section.contents().length -1];
var returned = section.document.wrapNodes({
- element1: aliceText,
- element2: lastDiv,
+ node1: aliceText,
+ node2: lastDiv,
_with: {tagName: 'header'}
});
@@ -441,8 +564,8 @@ describe('smartxml', function() {
div3 = section.contents()[2];
section.document.wrapNodes({
- element1: div2,
- element2: div3,
+ node1: div2,
+ node2: div3,
_with: {tagName: 'header'}
});
@@ -653,6 +776,60 @@ describe('smartxml', function() {
expect(event.type).to.equal('nodeMoved');
expect(event.meta.node.sameNode(inserted)).to.be.true;
});
+
+ it('emits nodeDetached and nodeAdded when replacing root node with another', function() {
+ var doc = getDocumentFromXML(''),
+ oldRoot = doc.root,
+ spy = sinon.spy();
+
+ doc.on('change', spy);
+
+ doc.root.replaceWith({tagName: 'b'});
+
+ expect(spy.callCount).to.equal(2);
+
+ var event1 = spy.args[0][0],
+ event2 = spy.args[1][0];
+
+ expect(event1.type).to.equal('nodeDetached');
+ expect(event1.meta.node.sameNode(oldRoot)).to.equal(true, 'root node in nodeDetached event metadata');
+ expect(event2.type).to.equal('nodeAdded');
+ expect(event2.meta.node.sameNode(doc.root)).to.equal(true, 'new root node in nodelAdded event meta');
+ });
+
+
+ ['append', 'prepend', 'before', 'after'].forEach(function(insertionMethod) {
+ it('emits nodeDetached for node moved from a document tree to out of document node ' + insertionMethod, function() {
+ var doc = getDocumentFromXML(''),
+ a = doc.root.contents()[0],
+ spy = sinon.spy();
+
+ doc.on('change', spy);
+
+ var newNode = doc.createDocumentNode({tagName: 'b'}),
+ newNodeInner = newNode.append({tagName:'c'});
+
+ newNodeInner[insertionMethod](a);
+
+ var event = spy.args[0][0];
+ expect(event.type).to.equal('nodeDetached');
+ expect(event.meta.node.sameNode(a));
+ });
+
+ it('doesn\'t emit nodeDetached event for already out of document node moved to out of document node' + insertionMethod, function() {
+ var doc = getDocumentFromXML(''),
+ spy = sinon.spy();
+
+ doc.on('change', spy);
+
+ var newNode = doc.createDocumentNode({tagName: 'b'});
+ newNode.append({tagName:'c'});
+
+ expect(spy.callCount).to.equal(0);
+ });
+ });
+
+
});
describe('Traversing', function() {
@@ -672,7 +849,9 @@ describe('smartxml', function() {
c = b.contents()[0];
var parents = c.parents();
- expect(parents).to.eql([b,a]);
+ // @@
+ expect(parents[0].sameNode(b)).to.be.true;
+ expect(parents[1].sameNode(a)).to.be.true;
});
});
@@ -730,6 +909,439 @@ describe('smartxml', function() {
});
});
+ describe('Extension API', function() {
+ var doc, extension, elementNode, textNode;
+
+ beforeEach(function() {
+ doc = getDocumentFromXML('');
+ elementNode = doc.root;
+ textNode = doc.root.contents()[0];
+ extension = {};
+
+ expect(elementNode.testTransformation).to.be.undefined;
+ expect(textNode.testTransformation).to.be.undefined;
+ expect(doc.testTransformation).to.be.undefined;
+
+ expect(doc.testMethod).to.be.undefined;
+ expect(elementNode.testMethod).to.be.undefined;
+ expect(textNode.testMethod).to.be.undefined;
+ expect(elementNode.elementTestMethod).to.be.undefined;
+ expect(textNode.textTestMethod).to.be.undefined;
+ });
+
+ it('allows adding method to a document', function() {
+ extension = {document: {methods: {
+ testMethod: function() { return this; }
+ }}};
+
+ doc.registerExtension(extension);
+ expect(doc.testMethod()).to.equal(doc, 'context is set to a document instance');
+ });
+
+ it('allows adding transformation to a document', function() {
+ extension = {document: {transformations: {
+ testTransformation: function() { return this; },
+ testTransformation2: {impl: function() { return this;}}
+ }}};
+
+ doc.registerExtension(extension);
+ expect(doc.testTransformation()).to.equal(doc, 'context is set to a document instance');
+ expect(doc.testTransformation2()).to.equal(doc, 'context is set to a document instance');
+ });
+
+ it('allows adding method to a DocumentNode instance', function() {
+ extension = {
+ documentNode: {
+ methods: {
+ testMethod: function() { return this; }
+ }
+ },
+ textNode: {
+ methods: {
+ textTestMethod: function() { return this; }
+ }
+ },
+ elementNode: {
+ methods: {
+ elementTestMethod: function() { return this; }
+ }
+ }
+ };
+
+ doc.registerExtension(extension);
+
+ /* refresh */
+ elementNode = doc.root;
+ textNode = doc.root.contents()[0];
+
+ expect(elementNode.testMethod().sameNode(elementNode)).to.equal(true, 'context is set to a node instance');
+ expect(textNode.testMethod().sameNode(textNode)).to.equal(true, 'context is set to a node instance');
+
+ expect(elementNode.elementTestMethod().sameNode(elementNode)).to.be.true;
+ expect(elementNode.textTestMethod).to.be.undefined;
+
+ expect(textNode.textTestMethod().sameNode(textNode)).to.be.true;
+ expect(textNode.elementTestMethod).to.be.undefined;
+ });
+
+ it('allows adding transformation to a DocumentNode', function() {
+ extension = {
+ documentNode: {
+ transformations: {
+ testTransformation: function() { return this; },
+ testTransformation2: {impl: function() { return this;}}
+ }
+ },
+ textNode: {
+ transformations: {
+ textTestTransformation: function() { return this; }
+ }
+ },
+ elementNode: {
+ transformations: {
+ elementTestTransformation: function() { return this; }
+ }
+ }
+ };
+
+ doc.registerExtension(extension);
+
+ /* refresh */
+ elementNode = doc.root;
+ textNode = doc.root.contents()[0];
+
+ expect(elementNode.testTransformation().sameNode(elementNode)).to.equal(true, '1');
+ expect(elementNode.testTransformation2().sameNode(elementNode)).to.equal(true, '2');
+ expect(textNode.testTransformation().sameNode(textNode)).to.equal(true, '3');
+ expect(textNode.testTransformation2().sameNode(textNode)).to.equal(true, '4');
+
+ expect(elementNode.elementTestTransformation().sameNode(elementNode)).to.be.true;
+ expect(elementNode.textTestTransformation).to.be.undefined;
+
+ expect(textNode.textTestTransformation().sameNode(textNode)).to.be.true;
+ expect(textNode.elementTestTransfomation).to.be.undefined;
+ });
+
+ it('allows text/element node methods and transformations to access node and transormations on document node', function() {
+
+ var doc = getDocumentFromXML('text
');
+
+ doc.registerExtension({
+ documentNode: {
+ methods: {
+ test: function() {
+ return 'super';
+ }
+ },
+ transformations: {
+ testT: function() {
+ return 'super_trans';
+ }
+ }
+ },
+ elementNode: {
+ methods: {
+ test: function() {
+ return 'element_sub_' + this.__super__.test();
+ }
+ },
+ transformations: {
+ testT: function() {
+ return 'element_trans_sub_' + this.__super__.testT();
+ }
+ }
+ },
+ textNode: {
+ methods: {
+ test: function() {
+ return 'text_sub_' + this.__super__.test();
+ }
+ },
+ transformations: {
+ testT: function() {
+ return 'text_trans_sub_' + this.__super__.testT();
+ }
+ }
+ }
+ });
+
+ var textNode = doc.root.contents()[0];
+
+ expect(doc.root.test()).to.equal('element_sub_super');
+ expect(textNode.test()).to.equal('text_sub_super');
+ expect(doc.root.testT()).to.equal('element_trans_sub_super_trans');
+ expect(textNode.testT()).to.equal('text_trans_sub_super_trans');
+ });
+ });
+
+ describe('Undo/redo', function() {
+
+ it('smoke tests', function() {
+ var doc = getDocumentFromXML('Alice
'),
+ textNode = doc.root.contents()[0];
+
+ expect(doc.undoStack).to.have.length(0);
+
+ textNode.wrapWith({tagName: 'span', start:1, end:2});
+ expect(doc.undoStack).to.have.length(1, '1');
+ expect(doc.toXML()).to.equal('Alice
');
+
+ doc.undo();
+ expect(doc.undoStack).to.have.length(0, '2');
+ expect(doc.toXML()).to.equal('Alice
');
+
+ doc.redo();
+ expect(doc.undoStack).to.have.length(1, '3');
+ expect(doc.toXML()).to.equal('Alice
');
+
+ doc.undo();
+ expect(doc.undoStack).to.have.length(0, '4');
+ expect(doc.toXML()).to.equal('Alice
');
+
+ doc.undo();
+ expect(doc.undoStack).to.have.length(0, '5');
+ expect(doc.toXML()).to.equal('Alice
');
+ });
+
+ it('smoke tests 2', function() {
+ var doc = getDocumentFromXML('Alice
'),
+ textNode = doc.root.contents()[0],
+ path = textNode.getPath();
+
+ textNode.setText('Alice ');
+ textNode.setText('Alice h');
+ textNode.setText('Alice ha');
+ textNode.setText('Alice has');
+
+ expect(textNode.getText()).to.equal('Alice has');
+
+ doc.undo();
+ expect(doc.root.contents()[0].getText()).to.equal('Alice ha', '1');
+
+ doc.undo();
+ expect(doc.root.contents()[0].getText()).to.equal('Alice h', '2');
+
+ doc.redo();
+ expect(doc.root.contents()[0].getText()).to.equal('Alice ha', '3');
+
+ doc.redo();
+ expect(doc.root.contents()[0].getText()).to.equal('Alice has', '4');
+
+ doc.undo();
+ doc.undo();
+ textNode = doc.getNodeByPath(path);
+ textNode.setText('Cat');
+ doc.undo();
+ textNode = doc.getNodeByPath(path);
+ expect(textNode.getText()).to.equal('Alice h');
+ });
+
+
+ var sampleMethod = function(val) {
+ this._$.attr('x', val);
+ };
+
+ var transformations = {
+ 'unaware': sampleMethod,
+ 'returning change root': {
+ impl: sampleMethod,
+ getChangeRoot: function() {
+ return this.context;
+ }
+ },
+ 'implementing undo operation': {
+ impl: function(t, val) {
+ t.oldVal = this.getAttr('x');
+ sampleMethod.call(this, val);
+ },
+ undo: function(t) {
+ this.setAttr('x', t.oldVal);
+ }
+ }
+ };
+
+ _.pairs(transformations).forEach(function(pair) {
+ var name = pair[0],
+ transformaton = pair[1];
+
+ describe(name + ' transformation: ', function() {
+ var doc, node, nodePath;
+
+ beforeEach(function() {
+ doc = getDocumentFromXML('
');
+
+ doc.registerExtension({elementNode: {transformations: {
+ test: transformaton
+ }}});
+
+ node = doc.root.contents()[0];
+ nodePath = node.getPath();
+ });
+
+ it('transforms as expected', function() {
+ node.test('new');
+ expect(node.getAttr('x')).to.equal('new');
+ });
+
+ it('can be undone', function() {
+ node.test('new');
+ doc.undo();
+ node = doc.getNodeByPath(nodePath);
+ expect(node.getAttr('x')).to.equal('old');
+ });
+
+ it('can be undone and then redone', function() {
+ node.test('new');
+ doc.undo();
+ doc.redo();
+ node = doc.getNodeByPath(nodePath);
+ expect(node.getAttr('x')).to.equal('new');
+ });
+
+ it('handles a sample scenario', function() {
+ doc.root.contents()[0].test('1');
+ doc.root.contents()[0].test('2');
+ doc.root.contents()[0].test('3');
+ doc.root.contents()[0].test('4');
+ doc.root.contents()[0].test('5');
+
+ expect(doc.root.contents()[0].getAttr('x')).to.equal('5', 'after initial transformations');
+ doc.undo();
+ expect(doc.root.contents()[0].getAttr('x')).to.equal('4', 'undo 1.1');
+ doc.undo();
+ expect(doc.root.contents()[0].getAttr('x')).to.equal('3', 'undo 1.2');
+ doc.redo();
+ expect(doc.root.contents()[0].getAttr('x')).to.equal('4', 'redo 1.1');
+ doc.redo();
+ expect(doc.root.contents()[0].getAttr('x')).to.equal('5', 'redo 1.2');
+ doc.undo();
+ expect(doc.root.contents()[0].getAttr('x')).to.equal('4', 'undo 2.1');
+ doc.root.contents()[0].test('10');
+ expect(doc.root.contents()[0].getAttr('x')).to.equal('10', 'additional transformation');
+ expect(doc.redoStack.length).to.equal(0, 'transformation cleared redo stack');
+ doc.redo();
+ expect(doc.root.contents()[0].getAttr('x')).to.equal('10', 'empty redoStack so redo was noop');
+ doc.undo();
+ expect(doc.root.contents()[0].getAttr('x')).to.equal('4', 'undoing additional transformation');
+ doc.redo();
+ expect(doc.root.contents()[0].getAttr('x')).to.equal('10', 'redoing additional transformation');
+ });
+ });
+ });
+
+ it('smoke tests nested transformations', function() {
+ var doc = getDocumentFromXML('');
+
+ doc.registerExtension({elementNode: {transformations: {
+ nested: function(v) {
+ this._$.attr('innerAttr', v);
+ },
+ outer: function(v) {
+ this.nested(v);
+ this._$.attr('outerAttr', v);
+ }
+ }}});
+
+ doc.root.outer('test1');
+ doc.root.outer('test2');
+
+ expect(doc.root.getAttr('innerAttr')).to.equal('test2');
+ expect(doc.root.getAttr('outerAttr')).to.equal('test2');
+
+ doc.undo();
+
+ expect(doc.root.getAttr('innerAttr')).to.equal('test1');
+ expect(doc.root.getAttr('outerAttr')).to.equal('test1');
+
+ doc.undo();
+
+ expect(doc.root.getAttr('innerAttr')).to.equal(undefined);
+ expect(doc.root.getAttr('outerAttr')).to.equal(undefined);
+
+ doc.redo();
+
+ expect(doc.root.getAttr('innerAttr')).to.equal('test1');
+ expect(doc.root.getAttr('outerAttr')).to.equal('test1');
+
+ doc.redo();
+
+ expect(doc.root.getAttr('innerAttr')).to.equal('test2');
+ expect(doc.root.getAttr('outerAttr')).to.equal('test2');
+
+ });
+
+ describe('Transactions', function() {
+ it('allows to undo/redo series of transformations at once', function() {
+ var doc = getDocumentFromXML('');
+
+ doc.registerExtension({
+ elementNode: {transformations: {
+ test: function(v) {
+ this.setAttr('test', v);
+ }
+ }}
+ });
+
+ doc.startTransaction();
+ doc.root.test('1');
+ doc.root.test('2');
+ doc.root.test('3');
+ doc.endTransaction();
+
+ doc.undo();
+ expect(doc.root.getAttr('test'), '1');
+ doc.redo();
+ expect(doc.root.getAttr('test'), '3');
+ doc.undo();
+ expect(doc.root.getAttr('test'), '1');
+ doc.redo();
+ expect(doc.root.getAttr('test'), '3');
+ });
+
+ it('ignores empty transactions', function() {
+ var doc = getDocumentFromXML('');
+ doc.startTransaction();
+ doc.endTransaction();
+ expect(doc.undoStack).to.have.length(0, 'empty transaction doesn\'t get pushed into undo stack');
+ });
+
+ it('doesn\'t break on optimizations', function() {
+ // This is a smoke test checking if optimizations made to transaction undoing
+ // doesnt't break anything.
+ var doc = getDocumentFromXML('');
+
+ doc.registerExtension({
+ elementNode: {transformations: {
+ unaware: function(v) {
+ this.setAttr('unware', v);
+ },
+ smart: {
+ impl: function(t, v) {
+ t.oldVal = this.getAttr('smart');
+ this.setAttr('smart', v);
+ },
+ undo: function(t) {
+ this.setAttr('smart', t.oldVal);
+ }
+ }
+ }}
+ });
+
+ doc.startTransaction();
+ doc.root.smart('2');
+ doc.root.unaware('2');
+ doc.root.smart('3');
+ doc.root.unaware('3');
+ doc.endTransaction();
+
+ doc.undo();
+
+ expect(doc.root.getAttr('smart')).to.equal('1');
+ expect(doc.root.getAttr('unaware')).to.equal('1');
+ });
+ });
+ });
+
});
});
\ No newline at end of file