1be4cc125aa13f2b6626915b19f768a6f2727f97
[fnpeditor.git] / src / smartxml / smartxml.test.js
1 define([
2     'libs/chai',
3     'libs/sinon',
4     'libs/underscore',
5     './smartxml.js'
6 ], function(chai, sinon, _, smartxml) {
7     
8 'use strict';
9 /*jshint expr:true */
10 /* global describe, it, beforeEach, Node, DOMParser */
11
12 var expect = chai.expect;
13
14
15 var getDocumentFromXML = function(xml) {
16     return smartxml.documentFromXML(xml);
17 };
18
19 var elementNodeFromParams = function(params) {
20     return smartxml.elementNodeFromXML('<' + params.tag + '></' + params.tag + '>');
21 };
22
23 var elementNodeFromXML = function(xml) {
24     return smartxml.elementNodeFromXML(xml);
25 };
26
27
28 describe('smartxml', function() {
29
30     describe('Basic Document properties', function() {
31         it('exposes its root element', function() {
32             var doc = getDocumentFromXML('<div></div>');
33             expect(doc.root.getTagName()).to.equal('div');
34         });
35
36         it('can resets its content entirely', function() {
37             var doc = getDocumentFromXML('<div></div>');
38
39             expect(doc.root.getTagName()).to.equal('div');
40
41             doc.loadXML('<header></header>');
42             expect(doc.root.getTagName()).to.equal('header');
43         });
44
45         it('knows if it contains an ElementNode in its tree', function() {
46             var doc = getDocumentFromXML('<root><a></a>text</root>'),
47                 root = doc.root,
48                 a = root.contents()[0],
49                 text = root.contents()[1];
50
51             expect(doc.containsNode(root)).to.equal(true, 'contains its root');
52             expect(doc.containsNode(a)).to.equal(true, 'contains Element Node');
53             expect(doc.containsNode(text)).to.equal(true, 'contains Text Node');
54         });
55
56         it('creates text nodes', function() {
57             var doc = getDocumentFromXML('<div></div>'),
58                 emptyTextNode = doc.createDocumentNode({text:''}),
59                 nonEmptyTextNode = doc.createDocumentNode({text: 'alice'});
60             expect(emptyTextNode.getText()).to.equal('', 'empty ok');
61             expect(nonEmptyTextNode.getText()).to.equal('alice', 'non empty ok');
62         });
63
64         it('creates nodes from xml strings', function() {
65             var doc = getDocumentFromXML('<div></div>'),
66                 node = doc.createDocumentNode('<a>Alice<b></b></a>');
67             expect(node.getTagName()).to.equal('a');
68             expect(node.contents().length).to.equal(2);
69             expect(node.contents()[0].getText()).to.equal('Alice');
70             expect(node.contents()[1].getTagName()).to.equal('b');
71         });
72
73         describe('Retrieving node by path', function() {
74             it('passes smoke tests', function() {
75                 var doc = getDocumentFromXML('<root><a><b>c</b></a>');
76                 expect(doc.getNodeByPath([0]).sameNode(doc.root.contents()[0])).to.be.true;
77                 expect(doc.getNodeByPath([0,0]).sameNode(doc.root.contents()[0].contents()[0])).to.be.true;
78             });
79             it('treats empty path as a root path', function() {
80                 var doc = getDocumentFromXML('<root></root>');
81                 expect(doc.getNodeByPath([]).sameNode(doc.root)).to.be.true;
82             });
83             it('returns undefined for non existing paths', function() {
84                 var doc = getDocumentFromXML('<root><a></a></root>');
85                 expect(doc.getNodeByPath([1])).to.be.undefined;
86                 expect(doc.getNodeByPath([0,1])).to.be.undefined;
87                 expect(doc.getNodeByPath([10,1])).to.be.undefined;
88             });
89         });
90     });
91
92     describe('DocumentNode', function() {
93         it('can be cloned', function() {
94             var doc = getDocumentFromXML('<div>Alice</div>'),
95                 text = doc.root.contents()[0],
96                 clone, suffix;
97
98             [doc.root, text].forEach(function(node) {
99                 suffix = ' (' + (node.nodeType === Node.TEXT_NODE ? 'text' : 'element')  + ')';
100                 clone = node.clone();
101                 expect(doc.containsNode(clone)).to.equal(false, 'clone is not contained in a document' + suffix);
102                 expect(node.sameNode(clone)).to.equal(false, 'clone is not same node as its originator' + suffix);
103                 expect(node.nativeNode.isEqualNode(clone.nativeNode)).to.equal(true, 'clone is identical as its originator' + suffix);
104             });
105         });
106
107         it('can be cloned with its contents and its contents data', function() {
108             var doc = getDocumentFromXML('<root><div></div></root>'),
109                 root = doc.root,
110                 div = root.contents()[0];
111
112             var ClonableObject = function(arg) {
113                 this.arg = arg;
114             };
115             ClonableObject.prototype.clone = function() {
116                 return new ClonableObject(this.arg);
117             };
118
119             div.setData('key', 'value');
120             div.setData('clonableObject', new ClonableObject('test'));
121
122             var rootClone = root.clone(),
123                 divClone = rootClone.contents()[0],
124                 stringClone = divClone.getData('key'),
125                 objClone = divClone.getData('clonableObject');
126
127             expect(stringClone).to.equal('value');
128             expect(objClone.arg).to.equal('test', 'clonable object got copied');
129             expect(objClone !== div.getData('clonableObject')).to.be.equal(true, 'copy of the clonable object is a new object');
130         });
131
132         it('knows its path in the document tree', function() {
133             var doc = getDocumentFromXML('<root><a><b><c></c>text</b></a></root>'),
134                 root = doc.root,
135                 a = root.contents()[0],
136                 b = a.contents()[0],
137                 text = b.contents()[1];
138
139             expect(root.getPath()).to.eql([], 'path of the root element is empty');
140             expect(a.getPath()).to.eql([0]);
141             expect(b.getPath()).to.eql([0, 0]);
142             expect(text.getPath()).to.eql([0,0,1]);
143
144             /* Paths relative to a given ancestor */
145             expect(text.getPath(root)).to.eql([0,0,1]);
146             expect(text.getPath(a)).to.eql([0,1]);
147             expect(text.getPath(b)).to.eql([1]);
148         });
149     });
150
151     describe('Basic ElementNode properties', function() {
152         it('exposes node contents', function() {
153             var node = elementNodeFromXML('<node>Some<node>text</node>is here</node>'),
154                 contents = node.contents();
155
156             expect(contents).to.have.length(3);
157             expect(contents[0].nodeType).to.equal(Node.TEXT_NODE, 'text node 1');
158             expect(contents[1].nodeType).to.equal(Node.ELEMENT_NODE, 'element node 1');
159             expect(contents[2].nodeType).to.equal(Node.TEXT_NODE, 'text node 2');
160         });
161
162         describe('Storing custom data', function() {
163             var node;
164
165             beforeEach(function() {
166                 node = elementNodeFromXML('<div></div>');
167             });
168
169             it('can append single value', function() {
170                 node.setData('key', 'value');
171                 expect(node.getData('key')).to.equal('value');
172             });
173
174             it('can overwrite the whole data', function() {
175                 node.setData('key1', 'value1');
176                 node.setData({key2: 'value2'});
177                 expect(node.getData('key2')).to.equal('value2');
178             });
179
180             it('can fetch the whole data at once', function() {
181                 node.setData({key1: 'value1', key2: 'value2'});
182                 expect(node.getData()).to.eql({key1: 'value1', key2: 'value2'});
183             });
184
185             it('can remove specific data', function() {
186                 node.setData('key', 'value');
187                 node.setData('key', undefined);
188                 expect(node.getData('key')).to.be.undefined;
189             });
190         });
191
192         describe('Changing node tag', function() {
193
194             it('can change tag name', function() {
195                 var node = elementNodeFromXML('<div></div>');
196                 node = node.setTag('span');
197                 expect(node.getTagName()).to.equal('span');
198             });
199
200             describe('Implementation specific expectations', function() {
201                 it('keeps custom data', function() {
202                     var node = elementNodeFromXML('<div></div>');
203
204                     node.setData('key', 'value');
205                     node = node.setTag('header');
206                     
207                     expect(node.getTagName()).to.equal('header');
208                     expect(node.getData()).to.eql({key: 'value'});
209                 });
210
211                 it('can change document root tag name', function() {
212                     var doc = getDocumentFromXML('<div></div>');
213                     doc.root.setTag('span');
214                     expect(doc.root.getTagName()).to.equal('span');
215                 });
216
217                 it('keeps node contents', function() {
218                     var node = elementNodeFromXML('<div><div></div></div>');
219                     node = node.setTag('header');
220                     expect(node.contents()).to.have.length(1);
221                 });
222             });
223         });
224
225         describe('Setting node attributes', function() {
226             it('can set node attribute', function() {
227                 var node = elementNodeFromXML('<div></div>');
228
229                 node.setAttr('key', 'value');
230                 expect(node.getAttr('key')).to.equal('value');
231             });
232             it('emits nodeAttrChange event', function() {
233                 var node = elementNodeFromXML('<div key="value1"></div>'),
234                     spy = sinon.spy();
235
236                 node.document.on('change', spy);
237                 node.setAttr('key', 'value2');
238                 var event = spy.args[0][0];
239
240                 expect(event.type).to.equal('nodeAttrChange');
241                 expect(event.meta.node.sameNode(node)).to.be.true;
242                 expect(event.meta.attr).to.equal('key');
243                 expect(event.meta.oldVal).to.equal('value1');
244             });
245         });
246
247         describe('Searching for the last child text node', function() {
248             [
249                 '<div>xxx<div></div>last</div>',
250                 '<div><div>last</div></div>',
251                 '<div>xxx<div>last</div><div></div></div>'
252             ].forEach(function(xml, i) {
253                 var example = 'example ' + i;
254                 it('returns last child text node ' + example + ')', function() {
255                     var doc = getDocumentFromXML(xml),
256                         lastTextNode = doc.root.getLastTextNode();
257                     expect(lastTextNode.getText()).to.equal('last', example);
258                 });
259             });
260         });
261
262         describe('Putting nodes around', function() {
263             it('will not allow to put node before or after root node', function() {
264                 var doc = getDocumentFromXML('<root></root>'),
265                     spy = sinon.spy(),
266                     root = doc.root,
267                     result;
268
269                 doc.on('change', spy);
270
271                 result = doc.root.before({tagName: 'test'});
272
273                 expect(spy.callCount).to.equal(0);
274                 expect(result).to.undefined;
275
276                 result = doc.root.after({tagName: 'test'});
277                 
278                 expect(spy.callCount).to.equal(0);
279                 expect(result).to.undefined;
280
281                 expect(doc.root.sameNode(root));
282             });
283         });
284     });
285
286     describe('Basic TextNode properties', function() {
287         it('can have its text set', function() {
288             var node = elementNodeFromXML('<div>Alice</div>'),
289                 textNode = node.contents()[0];
290
291             textNode.setText('Cat');
292             expect(textNode.getText()).to.equal('Cat');
293         });
294
295         it('emits nodeTextChange', function() {
296             var node = elementNodeFromXML('<div>Alice</div>'),
297                 textNode = node.contents()[0],
298                 spy = sinon.spy();
299
300             textNode.document.on('change', spy);
301             textNode.setText('Cat');
302
303             var event = spy.args[0][0];
304             expect(event.type).to.equal('nodeTextChange');
305         });
306
307         it('puts NodeElement after itself', function() {
308             var node = elementNodeFromXML('<div>Alice</div>'),
309                 textNode = node.contents()[0],
310                 returned = textNode.after({tagName:'div'});
311             expect(returned.sameNode(node.contents()[1])).to.be.true;
312         });
313
314         it('puts NodeElement before itself', function() {
315             var node = elementNodeFromXML('<div>Alice</div>'),
316                 textNode = node.contents()[0],
317                 returned = textNode.before({tagName:'div'});
318             expect(returned.sameNode(node.contents()[0])).to.be.true;
319         });
320
321         describe('Wrapping TextNode contents', function() {
322
323             it('wraps DocumentTextElement', function() {
324                 var node = elementNodeFromXML('<section>Alice</section>'),
325                     textNode = node.contents()[0];
326                 
327                 var returned = textNode.wrapWith({tagName: 'header'}),
328                     parent = textNode.parent(),
329                     parent2 = node.contents()[0];
330
331                 expect(returned.sameNode(parent)).to.be.equal(true, 'wrapper is a parent');
332                 expect(returned.sameNode(parent2)).to.be.equal(true, 'wrapper has a correct parent');
333                 expect(returned.getTagName()).to.equal('header');
334             });
335
336             describe('wrapping part of DocumentTextElement', function() {
337                 [{start: 5, end: 12}, {start: 12, end: 5}].forEach(function(offsets) {
338                     it('wraps in the middle ' + offsets.start + '/' + offsets.end, function() {
339                         var node = elementNodeFromXML('<section>Alice has a cat</section>'),
340                             textNode = node.contents()[0];
341                         
342                         var returned = textNode.wrapWith({tagName: 'header', attrs: {'attr1': 'value1'}, start: offsets.start, end: offsets.end}),
343                             contents = node.contents();
344
345                         expect(contents.length).to.equal(3);
346                         
347                         expect(contents[0].nodeType).to.be.equal(Node.TEXT_NODE, 'first node is text node');
348                         expect(contents[0].getText()).to.equal('Alice');
349
350                         expect(contents[1].sameNode(returned)).to.be.true;
351                         expect(returned.getTagName()).to.equal('header');
352                         expect(returned.getAttr('attr1')).to.equal('value1');
353                         expect(contents[1].contents().length).to.equal(1, 'wrapper has one node inside');
354                         expect(contents[1].contents()[0].getText()).to.equal(' has a ');
355
356                         expect(contents[2].nodeType).to.be.equal(Node.TEXT_NODE, 'third node is text node');
357                         expect(contents[2].getText()).to.equal('cat');
358                     });
359                 });
360
361                 it('wraps whole text inside DocumentTextElement if offsets span entire content', function() {
362                     var node = elementNodeFromXML('<section>Alice has a cat</section>'),
363                          textNode = node.contents()[0];
364                      
365                     textNode.wrapWith({tagName: 'header', start: 0, end: 15});
366                     
367                     var contents = node.contents();
368                     expect(contents.length).to.equal(1);
369                     expect(contents[0].getTagName()).to.equal('header');
370                     expect(contents[0].contents()[0].getText()).to.equal('Alice has a cat');
371                 });
372             });
373         });
374
375         describe('Dividing text node into two with element node', function() {
376                 it('can divide text node with element node, splitting text node into two', function() {
377                     var doc = getDocumentFromXML('<div>Alice has a cat</div>'),
378                         text = doc.root.contents()[0];
379
380                     var returned = text.divideWithElementNode({tagName: 'aside'}, {offset: 5}),
381                         contents = doc.root.contents(),
382                         lhsText = contents[0],
383                         rhsText = contents[2];
384
385                     expect(lhsText.getText()).to.equal('Alice');
386                     expect(returned.sameNode(contents[1]));
387                     expect(rhsText.getText()).to.equal(' has a cat');
388                 });
389
390                 it('treats dividing at the very end as appending after it', function() {
391                     var doc = getDocumentFromXML('<div>Alice has a cat</div>'),
392                         text = doc.root.contents()[0];
393
394
395                     var returned = text.divideWithElementNode({tagName: 'aside'}, {offset: 15}),
396                         contents = doc.root.contents(),
397                         textNode = contents[0],
398                         elementNode = contents[1];
399
400                     expect(contents.length).to.equal(2);
401                     expect(textNode.getText()).to.equal('Alice has a cat');
402                     expect(returned.sameNode(elementNode)).to.be.true;
403                     expect(elementNode.getTagName()).to.equal('aside');
404                 });
405
406                 it('treats dividing at the very beginning as prepending before it', function() {
407                     var doc = getDocumentFromXML('<div>Alice has a cat</div>'),
408                         text = doc.root.contents()[0];
409
410                     var returned = text.divideWithElementNode({tagName: 'aside'}, {offset: 0}),
411                         contents = doc.root.contents(),
412                         textNode = contents[1],
413                         elementNode = contents[0];
414
415                     expect(contents.length).to.equal(2);
416                     expect(textNode.getText()).to.equal('Alice has a cat');
417                     expect(returned.sameNode(elementNode)).to.be.true;
418                     expect(elementNode.getTagName()).to.equal('aside');
419                 });
420         });
421     });
422
423     describe('Manipulations', function() {
424
425         describe('detaching nodes', function() {
426             it('can detach document root node', function() {
427                 var doc = getDocumentFromXML('<div></div>');
428
429                 doc.root.detach();
430                 expect(doc.root).to.equal(null);
431             });
432         });
433
434         describe('replacing node with another one', function() {
435             it('replaces node with another one', function() {
436                 var doc = getDocumentFromXML('<div><a></a></div>'),
437                     a = doc.root.contents()[0];
438
439                 var c = a.replaceWith({tagName: 'b', attrs: {b:'1'}});
440
441                 expect(doc.root.contents()[0].sameNode(c));
442                 expect(c.getTagName()).to.equal('b');
443                 expect(c.getAttr('b')).to.equal('1');
444             });
445             it('can replace document root', function() {
446                 var doc = getDocumentFromXML('<div></div>');
447
448                 var header = doc.root.replaceWith({tagName: 'header'});
449
450                 expect(doc.root.sameNode(header)).to.be.true;
451                 expect(doc.containsNode(header)).to.be.true;
452             });
453         });
454
455         it('merges adjacent text nodes resulting from detaching an element node in between', function() {
456             var doc = getDocumentFromXML('<div>Alice <span>has</span>a cat</div>'),
457                 span = doc.root.contents()[1];
458
459             span.detach();
460
461             var rootContents = doc.root.contents();
462             expect(rootContents).to.have.length(1, 'one child left');
463             expect(rootContents[0].getText()).to.equal('Alice a cat');
464         });
465
466         it('merges adjacent text nodes resulting from moving an element node in between', function() {
467             var doc = getDocumentFromXML('<div><a></a>Alice <span>has</span>a cat</div>'),
468                 span = doc.root.contents()[2],
469                 a = doc.root.contents()[0];
470
471             a.append(span);
472
473             var rootContents = doc.root.contents();
474             expect(rootContents).to.have.length(2, 'one child left');
475             expect(rootContents[1].getText()).to.equal('Alice a cat');
476         });
477
478         it('inserts node at index', function() {
479             var doc = getDocumentFromXML('<div><a></a><b></b><c></c></div>'),
480                 b = doc.root.contents()[1];
481
482             var inserted = doc.root.insertAtIndex({tagName: 'test'}, 1);
483
484             expect(doc.root.contents()[1].sameNode(inserted)).to.equal(true, 'inserted node returned');
485             expect(b.getIndex()).to.equal(2, 'b node shifted right');
486         });
487
488         it('appends node when inserting node at index out of range', function() {
489             var doc = getDocumentFromXML('<div></div>');
490
491             var test1 = doc.root.insertAtIndex({tagName: 'test1'}, 0),
492                 test2 = doc.root.insertAtIndex({tagName: 'test1'}, 10);
493
494             expect(doc.root.contents()[0].sameNode(test1)).to.equal(true, 'inserting at index 0 of empty nodes appends node');
495             expect(doc.root.contents().length).to.equal(1, 'inserting at index out of range does nothing');
496             expect(test2).to.equal(undefined, 'inserting at index out of range returns undefined');
497         });
498
499         it('appends element node to another element node', function() {
500             var node1 = elementNodeFromParams({tag: 'div'}),
501                 node2 = elementNodeFromParams({tag: 'a'}),
502                 node3 = elementNodeFromParams({tag: 'p'});
503             node1.append(node2);
504             node1.append(node3);
505             expect(node1.contents()[0].sameNode(node2)).to.be.true;
506             expect(node1.contents()[1].sameNode(node3)).to.be.true;
507         });
508
509         it('prepends element node to another element node', function() {
510             var node1 = elementNodeFromParams({tag: 'div'}),
511                 node2 = elementNodeFromParams({tag: 'a'}),
512                 node3 = elementNodeFromParams({tag: 'p'});
513             node1.prepend(node2);
514             node1.prepend(node3);
515             expect(node1.contents()[0].sameNode(node3)).to.be.true;
516             expect(node1.contents()[1].sameNode(node2)).to.be.true;
517         });
518
519         describe('adding text nodes', function() {
520             it('merges text nodes on append', function() {
521                 var doc = getDocumentFromXML('<root>text1</root>'),
522                     returned;
523                 returned = doc.root.append({text: 'text2'});
524                 expect(doc.root.contents().length).to.equal(1);
525                 expect(returned.sameNode(doc.root.contents()[0])).to.equal(true, 'modified node returned');
526                 expect(doc.root.contents()[0].getText()).to.equal('text1text2');
527             });
528
529             it('merges text nodes on prepend', function() {
530                 var doc = getDocumentFromXML('<root>text1</root>'),
531                     returned;
532                 returned = doc.root.prepend({text: 'text2'});
533                 expect(doc.root.contents().length).to.equal(1);
534                 expect(returned.sameNode(doc.root.contents()[0])).to.equal(true, 'modified node returned');
535                 expect(doc.root.contents()[0].getText()).to.equal('text2text1');
536             });
537
538             it('merges text nodes on before text node', function() {
539                 var doc = getDocumentFromXML('<root>text1</root>'),
540                     textNode = doc.root.contents()[0],
541                     returned;
542                 returned = textNode.before({text: 'text2'});
543                 expect(doc.root.contents().length).to.equal(1);
544                 expect(returned.sameNode(doc.root.contents()[0])).to.equal(true, 'modified node returned');
545                 expect(doc.root.contents()[0].getText()).to.equal('text2text1');
546             });
547
548             it('merges text nodes on after text node', function() {
549                 var doc = getDocumentFromXML('<root>text1</root>'),
550                     textNode = doc.root.contents()[0],
551                     returned;
552                 returned = textNode.after({text: 'text2'});
553                 expect(doc.root.contents().length).to.equal(1);
554                 expect(returned.sameNode(doc.root.contents()[0])).to.equal(true, 'modified node returned');
555                 expect(doc.root.contents()[0].getText()).to.equal('text1text2');
556             });
557
558             it('merges text nodes on before element node', function() {
559                 var doc = getDocumentFromXML('<root>text1<div></div></root>'),
560                     textNode = doc.root.contents()[0],
561                     div = doc.root.contents()[1],
562                     returned;
563                 returned = div.before({text: 'text2'});
564                 expect(doc.root.contents().length).to.equal(2);
565                 expect(returned.sameNode(doc.root.contents()[0])).to.equal(true, 'modified node returned');
566                 expect(textNode.getText()).to.equal('text1text2');
567             });
568
569             it('merges text nodes on after element node', function() {
570                 var doc = getDocumentFromXML('<root><div></div>text1</root>'),
571                     textNode = doc.root.contents()[1],
572                     div = doc.root.contents()[0],
573                     returned;
574                 returned = div.after({text: 'text2'});
575                 expect(doc.root.contents().length).to.equal(2);
576                 expect(returned.sameNode(doc.root.contents()[1])).to.equal(true, 'modified node returned');
577                 expect(textNode.getText()).to.equal('text2text1');
578             });
579         });
580
581         it('wraps root element node with another element node', function() {
582             var node = elementNodeFromXML('<div></div>'),
583                 wrapper = elementNodeFromXML('<wrapper></wrapper>');
584
585             node.wrapWith(wrapper);
586             expect(node.parent().sameNode(wrapper)).to.be.true;
587             expect(node.document.root.sameNode(wrapper)).to.be.true;
588         });
589
590         it('wraps element node with another element node', function() {
591             var doc = getDocumentFromXML('<section><div></div></section>'),
592                 div = doc.root.contents()[0];
593
594             var wrapper = div.wrapWith({tagName: 'wrapper'});
595             expect(wrapper.sameNode(doc.root.contents()[0])).to.equal(true, '1');
596             expect(div.parent().sameNode(wrapper)).to.equal(true, '2');
597             expect(wrapper.contents()[0].sameNode(div)).to.equal(true, '3');
598         });
599
600         it('wraps element outside of document tree', function() {
601             var doc = getDocumentFromXML('<section><div></div></section>'),
602                 node = doc.createDocumentNode({tagName: 'node'});
603
604             node.wrapWith({tagName: 'wrapper'});
605             expect(node.parent().getTagName()).to.equal('wrapper');
606             expect(node.parent().contents()[0].sameNode(node)).to.be.true;
607             expect(doc.root.getTagName()).to.equal('section');
608         });
609
610         it('unwraps element node contents', function() {
611             var node = elementNodeFromXML('<div>Alice <div>has <span>propably</span> a cat</div>!</div>'),
612                 outerDiv = node.contents()[1];
613             
614             outerDiv.unwrapContent();
615
616             expect(node.contents().length).to.equal(3);
617             expect(node.contents()[0].getText()).to.equal('Alice has ');
618             expect(node.contents()[1].getTagName()).to.equal('span');
619             expect(node.contents()[2].getText()).to.equal(' a cat!');
620         });
621
622         it('removes parent-describing sibling nodes of unwrapped node', function() {
623             var doc = getDocumentFromXML('<root><div><a></a><x></x><a></a></div></root>');
624
625             doc.registerExtension({documentNode: {methods: {
626                 object: {
627                     describesParent: function() {
628                         return this.getTagName() === 'x';
629                     }
630                 }
631             }}});
632
633             var div = doc.root.contents()[0],
634                 x = div.contents()[1];
635
636             div.unwrapContent();
637             expect(doc.root.contents().length).to.equal(2);
638             expect(x.isInDocument()).to.be.false;
639         });
640
641         it('unwrap single element node from its parent', function() {
642             var doc = getDocumentFromXML('<div><a><b></b></a></div>'),
643                 div = doc.root,
644                 a = div.contents()[0],
645                 b = a.contents()[0];
646
647             var parent = b.unwrap();
648
649             expect(parent.sameNode(div)).to.equal(true, 'returns new parent');
650             expect(div.contents()).to.have.length(1, 'root contains only one node');
651             expect(div.contents()[0].sameNode(b)).to.equal(true, 'node got unwrapped');
652         });
653
654         it('unwrap single text node from its parent', function() {
655             var doc = getDocumentFromXML('<div>Some <span>text</span>!</div>'),
656                 div = doc.root,
657                 span = div.contents()[1],
658                 text = span.contents()[0];
659
660             var parent = text.unwrap();
661
662             expect(parent.sameNode(div)).to.equal(true, 'returns new parent');
663             expect(div.contents()).to.have.length(1, 'root contains only one node');
664             expect(div.contents()[0].getText()).to.equal('Some text!');
665         });
666
667         describe('Wrapping text', function() {
668             it('wraps text spanning multiple sibling TextNodes', function() {
669                 var section = elementNodeFromXML('<section>Alice has a <span>small</span> cat</section>'),
670                     wrapper = section.wrapText({
671                         _with: {tagName: 'span', attrs: {'attr1': 'value1'}},
672                         offsetStart: 6,
673                         offsetEnd: 4,
674                         textNodeIdx: [0,2]
675                     });
676
677                 expect(section.contents().length).to.equal(2);
678                 expect(section.contents()[0].nodeType).to.equal(Node.TEXT_NODE);
679                 expect(section.contents()[0].getText()).to.equal('Alice ');
680
681                 var wrapper2 = section.contents()[1];
682                 expect(wrapper2.sameNode(wrapper)).to.be.true;
683                 expect(wrapper.getTagName()).to.equal('span');
684
685                 var wrapperContents = wrapper.contents();
686                 expect(wrapperContents.length).to.equal(3);
687                 expect(wrapperContents[0].getText()).to.equal('has a ');
688
689                 expect(wrapperContents[1].nodeType).to.equal(Node.ELEMENT_NODE);
690                 expect(wrapperContents[1].contents().length).to.equal(1);
691                 expect(wrapperContents[1].contents()[0].getText()).to.equal('small');
692             });
693
694             it('keeps parent-describing nodes in place', function() {
695                 var doc = getDocumentFromXML('<root>Alice <x></x> probably <y></y> has a cat</root>');
696
697                 doc.registerExtension({documentNode: {methods: {
698                     object: {
699                         describesParent: function() {
700                             /* globals Node */
701                             return this.nodeType === Node.ELEMENT_NODE && this.getTagName() === 'x';
702                         }
703                     }
704                 }}});
705
706                 var root = doc.root,
707                     x = root.contents()[1],
708                     y = root.contents()[3];
709
710                 root.wrapText({
711                     _with: {tagName: 'span', attrs: {'attr1': 'value1'}},
712                     offsetStart: 1,
713                     offsetEnd: 4,
714                     textNodeIdx: [0,4]
715                 });
716
717                 expect(x.parent().sameNode(root)).to.be.true;
718                 expect(y.parent().getTagName()).to.equal('span');
719             });
720         });
721
722         describe('Wrapping Nodes', function() {
723             it('wraps multiple sibling nodes', function() {
724                 var section = elementNodeFromXML('<section>Alice<div>has</div><div>a cat</div></section>'),
725                     aliceText = section.contents()[0],
726                     firstDiv = section.contents()[1],
727                     lastDiv = section.contents()[section.contents().length -1];
728
729                 var returned = section.document.wrapNodes({
730                         node1: aliceText,
731                         node2: lastDiv,
732                         _with: {tagName: 'header'}
733                     });
734
735                 var sectionContentss = section.contents(),
736                     header = sectionContentss[0],
737                     headerContents = header.contents();
738
739                 expect(sectionContentss).to.have.length(1);
740                 expect(header.sameNode(returned)).to.equal(true, 'wrapper returned');
741                 expect(header.parent().sameNode(section)).to.be.true;
742                 expect(headerContents).to.have.length(3);
743                 expect(headerContents[0].sameNode(aliceText)).to.equal(true, 'first node wrapped');
744                 expect(headerContents[1].sameNode(firstDiv)).to.equal(true, 'second node wrapped');
745                 expect(headerContents[2].sameNode(lastDiv)).to.equal(true, 'third node wrapped');
746             });
747
748             it('wraps multiple sibling Elements - middle case', function() {
749                 var section = elementNodeFromXML('<section><div></div><div></div><div></div><div></div></section>'),
750                     div2 = section.contents()[1],
751                     div3 = section.contents()[2];
752
753                 section.document.wrapNodes({
754                         node1: div2,
755                         node2: div3,
756                         _with: {tagName: 'header'}
757                     });
758
759                 var sectionContentss = section.contents(),
760                     header = sectionContentss[1],
761                     headerChildren = header.contents();
762
763                 expect(sectionContentss).to.have.length(3);
764                 expect(headerChildren).to.have.length(2);
765                 expect(headerChildren[0].sameNode(div2)).to.equal(true, 'first node wrapped');
766                 expect(headerChildren[1].sameNode(div3)).to.equal(true, 'second node wrapped');
767             });
768
769             it('keeps parent-describing nodes in place', function() {
770                 var section = elementNodeFromXML('<section>Alice<x></x><div>a cat</div></section>');
771
772                 section.document.registerExtension({documentNode: {methods: {
773                     object: {
774                         describesParent: function() {
775                             return this.nodeType === Node.ELEMENT_NODE && this.getTagName() === 'x';
776                         }
777                     }
778                 }}});
779
780                 var aliceText = section.contents()[0],
781                     x = section.contents()[1],
782                     lastDiv = section.contents()[2];
783
784                 section.document.wrapNodes({
785                         node1: aliceText,
786                         node2: lastDiv,
787                         _with: {tagName: 'header'}
788                     });
789
790                 expect(x.parent().sameNode(section)).to.be.true;
791                 expect(aliceText.parent().getTagName()).to.equal('header');
792                 expect(lastDiv.parent().getTagName()).to.equal('header');
793             });
794         });
795
796     });
797
798     var getTextNodes = function(text, doc) {
799         /* globals Node */
800         var toret = [];
801         var search = function(node) {
802             node.contents().forEach(function(node) {
803                 if(node.nodeType === Node.TEXT_NODE) {
804                     if(node.getText() === text) {
805                         toret.push(node);
806                     }
807                 } else {
808                     search(node);
809                 }
810             });
811         };
812         search(doc.root);
813         return toret;
814     };
815
816     var getTextNode = function(text, doc) {
817         var nodes = getTextNodes(text, doc),
818             error;
819         if(nodes.length === 0) {
820             error = 'Text not found';
821         } else if(nodes.length > 1) {
822             error = 'Text not unique';
823         } else if(nodes[0].getText() !== text) {
824             error = 'I was trying to cheat your test :(';
825         }
826         if(error) {
827             throw new Error(error);
828         }
829         return nodes[0];
830     };
831
832     describe('Removing arbitrary text', function() {
833         it('removes within single text element', function() {
834             var doc = getDocumentFromXML('<div>Alice</div>'),
835                 text = getTextNode('Alice', doc);
836             doc.deleteText({
837                 from: {
838                     node: text,
839                     offset: 1
840                 },
841                 to: {
842                     node: text,
843                     offset: 4
844                 }
845             });
846             expect(doc.root.contents().length).to.equal(1);
847             expect(doc.root.contents()[0].getText()).to.equal('Ae');
848         });
849         it('removes across elements - 1', function() {
850             var doc = getDocumentFromXML('<div><a>aaa</a><b>bbb</b></div>');
851
852             doc.deleteText({
853                 from: {
854                     node: getTextNode('aaa', doc),
855                     offset: 2
856                 },
857                 to: {
858                     node: getTextNode('bbb', doc),
859                     offset: 2
860                 }
861             });
862
863             var contents = doc.root.contents();
864             expect(contents.length).to.equal(2);
865             expect(contents[0].contents()[0].getText()).to.equal('aa');
866             expect(contents[1].contents()[0].getText()).to.equal('b');
867         });
868         it('removes across elements - 2', function() {
869             var doc = getDocumentFromXML('<a><b><c>ccc</c></b>xxx</a>');
870             doc.deleteText({
871                 from: {
872                     node: getTextNode('ccc', doc),
873                     offset: 2
874                 },
875                 to: {
876                     node: getTextNode('xxx', doc),
877                     offset: 2
878                 }
879             });
880
881             var contents = doc.root.contents();
882             expect(contents.length).to.equal(2);
883             expect(contents[0].getTagName()).to.equal('b');
884             expect(contents[1].getText()).to.equal('x');
885             
886             var bContents = contents[0].contents();
887             expect(bContents.length).to.equal(1);
888             expect(bContents[0].getTagName()).to.equal('c');
889             expect(bContents[0].contents().length).to.equal(1);
890             expect(bContents[0].contents()[0].getText()).to.equal('cc');
891         });
892         it('remove across elements - 3 (merged text nodes)', function() {
893             var doc = getDocumentFromXML('<div>Alice <span>has</span> a cat</div>');
894             doc.deleteText({
895                 from: {
896                     node: getTextNode('Alice ', doc),
897                     offset: 1
898                 },
899                 to: {
900                     node: getTextNode(' a cat', doc),
901                     offset: 3
902                 }
903             });
904             var contents = doc.root.contents();
905             expect(contents.length).to.equal(1);
906             expect(contents[0].getText()).to.equal('Acat');
907         });
908         it('remove across elements - 4', function() {
909             var doc = getDocumentFromXML('<div>Alice <div>has <span>a</span> cat</div></div>');
910             doc.deleteText({
911                 from: {
912                     node: getTextNode('Alice ', doc),
913                     offset: 1
914                 },
915                 to: {
916                     node: getTextNode(' cat', doc),
917                     offset: 1
918                 }
919             });
920             var contents = doc.root.contents();
921             expect(contents.length).to.equal(2);
922             expect(contents[0].getText()).to.equal('A');
923             expect(contents[1].getTagName()).to.equal('div');
924             expect(contents[1].contents().length).to.equal(1);
925             expect(contents[1].contents()[0].getText()).to.equal('cat');
926         });
927         it('removes across elements - 5 (whole document)', function() {
928             var doc = getDocumentFromXML('<div>Alice <div>has <span>a</span> cat</div>!!!</div>');
929             doc.deleteText({
930                 from: {
931                     node: getTextNode('Alice ', doc),
932                     offset: 0
933                 },
934                 to: {
935                     node: getTextNode('!!!', doc),
936                     offset: 3
937                 }
938             });
939
940             expect(doc.root.getTagName()).to.equal('div');
941             expect(doc.root.contents().length).to.equal(1);
942             expect(doc.root.contents()[0].getText()).to.equal('');
943         });
944         it('removes nodes in between', function() {
945             var doc = getDocumentFromXML('<div><a>aaa<x>!</x></a>xxx<x></x><b><x>!</x>bbb</b></div>');
946             doc.deleteText({
947                 from: {
948                     node: getTextNode('aaa', doc),
949                     offset: 2
950                 },
951                 to: {
952                     node: getTextNode('bbb', doc),
953                     offset: 2
954                 }
955             });
956
957             var contents = doc.root.contents();
958             expect(contents.length).to.equal(2, 'two nodes survived');
959             expect(contents[0].getTagName()).to.equal('a');
960             expect(contents[1].getTagName()).to.equal('b');
961             expect(contents[0].contents().length).to.equal(1);
962             expect(contents[0].contents()[0].getText()).to.equal('aa');
963             expect(contents[1].contents().length).to.equal(1);
964             expect(contents[1].contents()[0].getText()).to.equal('b');
965         });
966         it('removes across elements - 6', function() {
967             var doc = getDocumentFromXML('<root><div>aaa<span>bbb</span>ccc</div><div>ddd</div></root>');
968             doc.deleteText({
969                 from: {
970                     node: getTextNode('aaa', doc),
971                     offset: 1
972                 },
973                 to: {
974                     node: getTextNode('ddd', doc),
975                     offset: 1
976                 }
977             }, {
978                 error: function(e) {throw e;}
979             });
980
981             var contents = doc.root.contents();
982             expect(contents.length).to.equal(2);
983             expect(contents[0].contents().length).to.equal(1);
984             expect(contents[0].contents()[0].getText()).to.equal('a');
985             expect(contents[1].contents().length).to.equal(1);
986             expect(contents[1].contents()[0].getText()).to.equal('dd');
987         });
988     });
989
990     describe('Splitting text', function() {
991     
992         it('splits TextNode\'s parent into two ElementNodes', function() {
993             var doc = getDocumentFromXML('<section><header>Some header</header></section>'),
994                 section = doc.root,
995                 text = section.contents()[0].contents()[0];
996
997             var returnedValue = text.split({offset: 5});
998             expect(section.contents().length).to.equal(2, 'section has two children');
999             
1000             var header1 = section.contents()[0];
1001             var header2 = section.contents()[1];
1002
1003             expect(header1.getTagName()).to.equal('header', 'first section child ok');
1004             expect(header1.contents().length).to.equal(1, 'first header has one child');
1005             expect(header1.contents()[0].getText()).to.equal('Some ', 'first header has correct content');
1006             expect(header2.getTagName()).to.equal('header', 'second section child ok');
1007             expect(header2.contents().length).to.equal(1, 'second header has one child');
1008             expect(header2.contents()[0].getText()).to.equal('header', 'second header has correct content');
1009
1010             expect(returnedValue.first.sameNode(header1)).to.equal(true, 'first node returned');
1011             expect(returnedValue.second.sameNode(header2)).to.equal(true, 'second node returned');
1012         });
1013
1014         it('leaves empty copy of ElementNode if splitting at the very beginning', function() {
1015                 var doc = getDocumentFromXML('<section><header>Some header</header></section>'),
1016                 section = doc.root,
1017                 text = section.contents()[0].contents()[0];
1018
1019                 text.split({offset: 0});
1020                 
1021                 var header1 = section.contents()[0];
1022                 var header2 = section.contents()[1];
1023
1024                 expect(header1.contents().length).to.equal(0);
1025                 expect(header2.contents()[0].getText()).to.equal('Some header');
1026         });
1027
1028         it('leaves empty copy of ElementNode if splitting at the very end', function() {
1029                 var doc = getDocumentFromXML('<section><header>Some header</header></section>'),
1030                 section = doc.root,
1031                 text = section.contents()[0].contents()[0];
1032
1033                 text.split({offset: 11});
1034                 
1035                 var header1 = section.contents()[0];
1036                 var header2 = section.contents()[1];
1037
1038                 expect(header1.contents()[0].getText()).to.equal('Some header');
1039                 expect(header2.contents().length).to.equal(0);
1040         });
1041
1042         it('keeps TextNodes\'s parent\'s children elements intact', function() {
1043             var doc = getDocumentFromXML('<section><header>A <span>fancy</span> and <span>nice</span> header</header></section>'),
1044                 section = doc.root,
1045                 header = section.contents()[0],
1046                 textAnd = header.contents()[2];
1047
1048             textAnd.split({offset: 2});
1049             
1050             var sectionContents = section.contents();
1051             expect(sectionContents.length).to.equal(2, 'Section has two children');
1052             expect(sectionContents[0].getTagName()).to.equal('header', 'First section node is a header');
1053             expect(sectionContents[1].getTagName()).to.equal('header', 'Second section node is a header');
1054
1055             var firstHeaderContents = sectionContents[0].contents();
1056             expect(firstHeaderContents.length).to.equal(3, 'First header has three children');
1057             expect(firstHeaderContents[0].getText()).to.equal('A ', 'First header starts with a text');
1058             expect(firstHeaderContents[1].getTagName()).to.equal('span', 'First header has span in the middle');
1059             expect(firstHeaderContents[2].getText()).to.equal(' a', 'First header ends with text');
1060
1061             var secondHeaderContents = sectionContents[1].contents();
1062             expect(secondHeaderContents.length).to.equal(3, 'Second header has three children');
1063             expect(secondHeaderContents[0].getText()).to.equal('nd ', 'Second header starts with text');
1064             expect(secondHeaderContents[1].getTagName()).to.equal('span', 'Second header has span in the middle');
1065             expect(secondHeaderContents[2].getText()).to.equal(' header', 'Second header ends with text');
1066         });
1067     });
1068
1069     describe('Events', function() {
1070         it('emits nodeDetached event on node detach', function() {
1071             var node = elementNodeFromXML('<div><div></div></div>'),
1072                 innerNode = node.contents()[0],
1073                 spy = sinon.spy();
1074             node.document.on('change', spy);
1075             
1076             var detached = innerNode.detach(),
1077                 event = spy.args[0][0];
1078
1079             expect(event.type).to.equal('nodeDetached');
1080             expect(event.meta.node.sameNode(detached, 'detached node in event meta'));
1081             expect(event.meta.parent.sameNode(node), 'original parent node in event meta');
1082         }),
1083
1084         it('emits nodeAdded event when appending new node', function() {
1085             var node = elementNodeFromXML('<div></div>'),
1086                 spy = sinon.spy();
1087             node.document.on('change', spy);
1088             
1089             var appended = node.append({tagName:'div'}),
1090                 event = spy.args[0][0];
1091             expect(event.type).to.equal('nodeAdded');
1092             expect(event.meta.node.sameNode(appended)).to.be.true;
1093         });
1094         
1095         it('emits nodeDetached/nodeAdded events with `move` flag when appending aready existing node', function() {
1096             var node = elementNodeFromXML('<div><a></a><b></b></div>'),
1097                 a = node.contents()[0],
1098                 b = node.contents()[1],
1099                 spy = sinon.spy();
1100             node.document.on('change', spy);
1101             
1102             var appended = a.append(b),
1103                 detachedEvent = spy.args[0][0],
1104                 addedEvent = spy.args[1][0];
1105
1106             expect(spy.callCount).to.equal(2);
1107             expect(detachedEvent.type).to.equal('nodeDetached');
1108             expect(detachedEvent.meta.node.sameNode(appended)).to.be.true;
1109             expect(detachedEvent.meta.move).to.equal(true, 'move flag set to true for nodeDetachedEvent');
1110             expect(addedEvent.type).to.equal('nodeAdded');
1111             expect(addedEvent.meta.node.sameNode(appended)).to.be.true;
1112             expect(addedEvent.meta.move).to.equal(true, 'move flag set to true for nodeAddedEvent');
1113
1114         });
1115         
1116         it('emits nodeAdded event when prepending new node', function() {
1117             var node = elementNodeFromXML('<div></div>'),
1118                 spy = sinon.spy();
1119             node.document.on('change', spy);
1120             
1121             var prepended = node.prepend({tagName:'div'}),
1122                 event = spy.args[0][0];
1123             expect(event.type).to.equal('nodeAdded');
1124             expect(event.meta.node.sameNode(prepended)).to.be.true;
1125         });
1126         
1127         it('emits nodeDetached/nodeAdded events with `move` flag when prepending aready existing node', function() {
1128             var node = elementNodeFromXML('<div><a></a><b></b></div>'),
1129                 a = node.contents()[0],
1130                 b = node.contents()[1],
1131                 spy = sinon.spy();
1132             node.document.on('change', spy);
1133
1134             var prepended = a.prepend(b),
1135                 detachedEvent = spy.args[0][0],
1136                 addedEvent = spy.args[1][0];
1137
1138             expect(spy.callCount).to.equal(2);
1139             expect(detachedEvent.type).to.equal('nodeDetached');
1140             expect(detachedEvent.meta.node.sameNode(prepended)).to.be.true;
1141             expect(detachedEvent.meta.move).to.equal(true, 'move flag set to true for nodeDetachedEvent');
1142             expect(addedEvent.type).to.equal('nodeAdded');
1143             expect(addedEvent.meta.node.sameNode(prepended)).to.be.true;
1144             expect(addedEvent.meta.move).to.equal(true, 'move flag set to true for nodeAddedEvent');
1145         });
1146         
1147         it('emits nodeAdded event when inserting node after another', function() {
1148             var node = elementNodeFromXML('<div><a></a></div>').contents()[0],
1149                 spy = sinon.spy();
1150             node.document.on('change', spy);
1151             
1152             var inserted = node.after({tagName:'div'}),
1153                 event = spy.args[0][0];
1154             expect(event.type).to.equal('nodeAdded');
1155             expect(event.meta.node.sameNode(inserted)).to.be.true;
1156         });
1157         
1158         it('emits nodeDetached/nodeAdded events with `move` flag when inserting aready existing node after another', function() {
1159             var node = elementNodeFromXML('<div><a></a><b></b></div>'),
1160                 a = node.contents()[0],
1161                 b = node.contents()[1],
1162                 spy = sinon.spy();
1163             node.document.on('change', spy);
1164             var inserted = b.after(a),
1165                 detachedEvent = spy.args[0][0],
1166                 addedEvent = spy.args[1][0];
1167
1168             expect(spy.callCount).to.equal(2);
1169             expect(detachedEvent.type).to.equal('nodeDetached');
1170             expect(detachedEvent.meta.node.sameNode(inserted)).to.be.true;
1171             expect(detachedEvent.meta.move).to.equal(true, 'move flag set to true for nodeDetachedEvent');
1172             expect(addedEvent.type).to.equal('nodeAdded');
1173             expect(addedEvent.meta.node.sameNode(inserted)).to.be.true;
1174             expect(addedEvent.meta.move).to.equal(true, 'move flag set to true for nodeAddedEvent');
1175         });
1176
1177         it('emits nodeAdded event when inserting node before another', function() {
1178             var node = elementNodeFromXML('<div><a></a></div>').contents()[0],
1179                 spy = sinon.spy();
1180             node.document.on('change', spy);
1181             
1182             var inserted = node.before({tagName:'div'}),
1183                 event = spy.args[0][0];
1184             expect(event.type).to.equal('nodeAdded');
1185             expect(event.meta.node.sameNode(inserted)).to.be.true;
1186         });
1187         
1188         it('emits nodeDetached/nodeAdded events with `move` flag when inserting aready existing node before another', function() {
1189             var node = elementNodeFromXML('<div><a></a><b></b></div>'),
1190                 a = node.contents()[0],
1191                 b = node.contents()[1],
1192                 spy = sinon.spy();
1193             node.document.on('change', spy);
1194             var inserted = a.before(b),
1195                 detachedEvent = spy.args[0][0],
1196                 addedEvent = spy.args[1][0];
1197
1198             expect(spy.callCount).to.equal(2);
1199             expect(detachedEvent.type).to.equal('nodeDetached');
1200             expect(detachedEvent.meta.node.sameNode(inserted)).to.be.true;
1201             expect(detachedEvent.meta.move).to.equal(true, 'move flag set to true for nodeDetachedEvent');
1202             expect(addedEvent.type).to.equal('nodeAdded');
1203             expect(addedEvent.meta.node.sameNode(inserted)).to.be.true;
1204             expect(addedEvent.meta.move).to.equal(true, 'move flag set to true for nodeAddedEvent');
1205         });
1206
1207         it('emits nodeDetached and nodeAdded when replacing root node with another', function() {
1208             var doc = getDocumentFromXML('<a></a>'),
1209                 oldRoot = doc.root,
1210                 spy = sinon.spy();
1211
1212             doc.on('change', spy);
1213
1214             doc.root.replaceWith({tagName: 'b'});
1215
1216             expect(spy.callCount).to.equal(2);
1217
1218             var event1 = spy.args[0][0],
1219                 event2 = spy.args[1][0];
1220
1221             expect(event1.type).to.equal('nodeDetached');
1222             expect(event1.meta.node.sameNode(oldRoot)).to.equal(true, 'root node in nodeDetached event metadata');
1223             expect(event2.type).to.equal('nodeAdded');
1224             expect(event2.meta.node.sameNode(doc.root)).to.equal(true, 'new root node in nodelAdded event meta');
1225         });
1226
1227
1228         ['append', 'prepend', 'before', 'after'].forEach(function(insertionMethod) {
1229             it('emits nodeDetached for node moved from a document tree to out of document node ' + insertionMethod, function() {
1230                 var doc = getDocumentFromXML('<div><a></a></div>'),
1231                     a = doc.root.contents()[0],
1232                     spy = sinon.spy();
1233
1234                 doc.on('change', spy);
1235
1236                 var newNode = doc.createDocumentNode({tagName: 'b'}),
1237                     newNodeInner = newNode.append({tagName:'c'});
1238
1239                 newNodeInner[insertionMethod](a);
1240
1241                 var event = spy.args[0][0];
1242                 expect(event.type).to.equal('nodeDetached');
1243                 expect(event.meta.node.sameNode(a));
1244             });
1245
1246             it('doesn\'t emit nodeDetached event for already out of document node moved to out of document node' + insertionMethod, function() {
1247                 var doc = getDocumentFromXML('<div><a></a></div>'),
1248                     spy = sinon.spy();
1249
1250                 doc.on('change', spy);
1251
1252                 var newNode = doc.createDocumentNode({tagName: 'b'});
1253                 newNode.append({tagName:'c'});
1254
1255                 expect(spy.callCount).to.equal(0);
1256             });
1257         });
1258
1259
1260     });
1261
1262     describe('Traversing', function() {
1263         describe('Basic', function() {
1264             it('can access node parent', function() {
1265                 var doc = getDocumentFromXML('<a><b></b></a>'),
1266                     a = doc.root,
1267                     b = a.contents()[0];
1268
1269                 expect(a.parent()).to.equal(null, 'parent of a root is null');
1270                 expect(b.parent().sameNode(a)).to.be.true;
1271             });
1272             it('can access node parents', function() {
1273                 var doc = getDocumentFromXML('<a><b><c></c></b></a>'),
1274                     a = doc.root,
1275                     b = a.contents()[0],
1276                     c = b.contents()[0];
1277
1278                 var parents = c.parents();
1279                 // @@
1280                 expect(parents[0].sameNode(b)).to.be.true;
1281                 expect(parents[1].sameNode(a)).to.be.true;
1282             });
1283         });
1284
1285         describe('finding sibling parents of two elements', function() {
1286             it('returns elements themself if they have direct common parent', function() {
1287                 var doc = getDocumentFromXML('<section><div><div>A</div><div>B</div></div></section>'),
1288                     wrappingDiv = doc.root.contents()[0],
1289                     divA = wrappingDiv.contents()[0],
1290                     divB = wrappingDiv.contents()[1];
1291
1292                 var siblingParents = doc.getSiblingParents({node1: divA, node2: divB});
1293
1294                 expect(siblingParents.node1.sameNode(divA)).to.equal(true, 'divA');
1295                 expect(siblingParents.node2.sameNode(divB)).to.equal(true, 'divB');
1296             });
1297
1298             it('returns sibling parents - example 1', function() {
1299                 var doc = getDocumentFromXML('<section>Alice <span>has a cat</span></section>'),
1300                     aliceText = doc.root.contents()[0],
1301                     span = doc.root.contents()[1],
1302                     spanText = span.contents()[0];
1303
1304                 var siblingParents = doc.getSiblingParents({node1: aliceText, node2: spanText});
1305
1306                 expect(siblingParents.node1.sameNode(aliceText)).to.equal(true, 'aliceText');
1307                 expect(siblingParents.node2.sameNode(span)).to.equal(true, 'span');
1308             });
1309
1310             it('returns node itself for two same nodes', function() {
1311                 var doc = getDocumentFromXML('<section><div></div></section>'),
1312                     div = doc.root.contents()[0];
1313
1314                 var siblingParents = doc.getSiblingParents({node1: div, node2: div});
1315                 expect(!!siblingParents.node1 && !!siblingParents.node2).to.equal(true, 'nodes defined');
1316                 expect(siblingParents.node1.sameNode(div)).to.be.equal(true, 'node1');
1317                 expect(siblingParents.node2.sameNode(div)).to.be.equal(true, 'node2');
1318             });
1319         });
1320     });
1321
1322     describe('Serializing document to WLXML', function() {
1323         it('keeps document intact when no changes have been made', function() {
1324             var xmlIn = '<section>Alice<div>has</div>a <span class="uri" meta-uri="http://cat.com">cat</span>!</section>',
1325                 doc = getDocumentFromXML(xmlIn),
1326                 xmlOut = doc.toXML();
1327
1328             var parser = new DOMParser(),
1329                 input = parser.parseFromString(xmlIn, 'application/xml').childNodes[0],
1330                 output = parser.parseFromString(xmlOut, 'application/xml').childNodes[0];
1331             
1332             expect(input.isEqualNode(output)).to.be.true;
1333         });
1334
1335         it('keeps entities intact', function() {
1336             var xmlIn = '<section>&lt; &gt;</section>',
1337                 doc = getDocumentFromXML(xmlIn),
1338                 xmlOut = doc.toXML();
1339             expect(xmlOut).to.equal(xmlIn);
1340         });
1341         it('keeps entities intact when they form html/xml', function() {
1342             var xmlIn = '<section>&lt;abc&gt;</section>',
1343                 doc = getDocumentFromXML(xmlIn),
1344                 xmlOut = doc.toXML();
1345             expect(xmlOut).to.equal(xmlIn);
1346         });
1347     });
1348
1349     describe('Extension API', function() {
1350         var doc, extension, elementNode, textNode;
1351
1352         beforeEach(function() {
1353             doc = getDocumentFromXML('<section>Alice<div class="test_class"></div></section>');
1354         });
1355
1356         it('allows adding method to a document', function() {
1357             extension = {document: {methods: {
1358                 testMethod: function() { return this; }
1359             }}};
1360
1361             doc.registerExtension(extension);
1362             expect(doc.testMethod()).to.equal(doc, 'context is set to a document instance');
1363         });
1364
1365         it('allows adding transformation to a document', function() {
1366             extension = {document: {transformations: {
1367                 testTransformation: function() { return this; },
1368                 testTransformation2: {impl: function() { return this;}}
1369             }}};
1370
1371             doc.registerExtension(extension);
1372             expect(doc.testTransformation()).to.equal(doc, 'context is set to a document instance');
1373             expect(doc.testTransformation2()).to.equal(doc, 'context is set to a document instance');
1374         });
1375
1376         it('allows adding method to a DocumentNode instance', function() {
1377             extension = {
1378                 documentNode: {
1379                     methods: {
1380                         testMethod: function() { return this; }
1381                     }
1382                 },
1383                 textNode: {
1384                     methods: {
1385                         textTestMethod: function() { return this; }
1386                     }
1387                 },
1388                 elementNode: {
1389                     methods: {
1390                         elementTestMethod: function() { return this; }
1391                     }
1392                 }
1393             };
1394
1395             doc.registerExtension(extension);
1396
1397             elementNode = doc.root;
1398             textNode = doc.root.contents()[0];
1399
1400             expect(elementNode.testMethod().sameNode(elementNode)).to.equal(true, 'context is set to a node instance');
1401             expect(textNode.testMethod().sameNode(textNode)).to.equal(true, 'context is set to a node instance');
1402
1403             expect(elementNode.elementTestMethod().sameNode(elementNode)).to.be.true;
1404             expect(elementNode.textTestMethod).to.be.undefined;
1405         
1406             expect(textNode.textTestMethod().sameNode(textNode)).to.be.true;
1407             expect(textNode.elementTestMethod).to.be.undefined;
1408         });
1409
1410         it('allows adding transformation to a DocumentNode', function() {
1411             extension = {
1412                 documentNode: {
1413                     transformations: {
1414                         testTransformation: function() { return this; },
1415                         testTransformation2: {impl: function() { return this;}}
1416                     }
1417                 },
1418                 textNode: {
1419                     transformations: {
1420                         textTestTransformation: function() { return this; }
1421                     }
1422                 },
1423                 elementNode: {
1424                     transformations: {
1425                         elementTestTransformation: function() { return this; }
1426                     }
1427                 }
1428             };
1429             
1430             doc.registerExtension(extension);
1431
1432             elementNode = doc.root;
1433             textNode = doc.root.contents()[0];
1434             
1435             expect(elementNode.testTransformation().sameNode(elementNode)).to.equal(true, '1');
1436             expect(elementNode.testTransformation2().sameNode(elementNode)).to.equal(true, '2');
1437             expect(textNode.testTransformation().sameNode(textNode)).to.equal(true, '3');
1438             expect(textNode.testTransformation2().sameNode(textNode)).to.equal(true, '4');
1439
1440             expect(elementNode.elementTestTransformation().sameNode(elementNode)).to.be.true;
1441             expect(elementNode.textTestTransformation).to.be.undefined;
1442         
1443             expect(textNode.textTestTransformation().sameNode(textNode)).to.be.true;
1444             expect(textNode.elementTestTransfomation).to.be.undefined;
1445         });
1446
1447         it('allows text/element node methods and transformations to access node and transormations on document node', function() {
1448
1449             var doc = getDocumentFromXML('<div>text</div>');
1450
1451             doc.registerExtension({
1452                 documentNode: {
1453                     methods: {
1454                         test: function() {
1455                             return 'super';
1456                         }
1457                     },
1458                     transformations: {
1459                         testT: function() {
1460                             return 'super_trans';
1461                         }
1462                     }
1463                 },
1464                 elementNode: {
1465                     methods: {
1466                         test: function() {
1467                             return 'element_sub_' + this.__super__.test();
1468                         }
1469                     },
1470                     transformations: {
1471                         testT: function() {
1472                             return 'element_trans_sub_' + this.__super__.testT();
1473                         }
1474                     }
1475                 },
1476                 textNode: {
1477                     methods: {
1478                         test: function() {
1479                             return 'text_sub_' + this.__super__.test();
1480                         }
1481                     },
1482                     transformations: {
1483                         testT: function() {
1484                             return 'text_trans_sub_' + this.__super__.testT();
1485                         }
1486                     }
1487                 }
1488             });
1489
1490             var textNode = doc.root.contents()[0];
1491
1492             expect(doc.root.test()).to.equal('element_sub_super');
1493             expect(textNode.test()).to.equal('text_sub_super');
1494             expect(doc.root.testT()).to.equal('element_trans_sub_super_trans');
1495             expect(textNode.testT()).to.equal('text_trans_sub_super_trans');
1496         });
1497     });
1498
1499     describe('Undo/redo', function() {
1500
1501         it('smoke tests', function() {
1502             var doc = getDocumentFromXML('<div>Alice</div>'),
1503                 textNode = doc.root.contents()[0];
1504
1505             expect(doc.undoStack).to.have.length(0);
1506             
1507             textNode.wrapWith({tagName: 'span', start:1, end:2});
1508             expect(doc.undoStack).to.have.length(1, '1');
1509             expect(doc.toXML()).to.equal('<div>A<span>l</span>ice</div>');
1510
1511             doc.undo();
1512             expect(doc.undoStack).to.have.length(0, '2');
1513             expect(doc.toXML()).to.equal('<div>Alice</div>');
1514
1515             doc.redo();
1516             expect(doc.undoStack).to.have.length(1, '3');
1517             expect(doc.toXML()).to.equal('<div>A<span>l</span>ice</div>');
1518
1519             doc.undo();
1520             expect(doc.undoStack).to.have.length(0, '4');
1521             expect(doc.toXML()).to.equal('<div>Alice</div>');
1522
1523             doc.undo();
1524             expect(doc.undoStack).to.have.length(0, '5');
1525             expect(doc.toXML()).to.equal('<div>Alice</div>');
1526         });
1527
1528         it('smoke tests 2', function() {
1529             var doc = getDocumentFromXML('<div>Alice</div>'),
1530                 textNode = doc.root.contents()[0],
1531                 path = textNode.getPath();
1532
1533             textNode.setText('Alice ');
1534             textNode.setText('Alice h');
1535             textNode.setText('Alice ha');
1536             textNode.setText('Alice has');
1537
1538             expect(textNode.getText()).to.equal('Alice has');
1539
1540             doc.undo();
1541             expect(doc.root.contents()[0].getText()).to.equal('Alice ha', '1');
1542
1543             doc.undo();
1544             expect(doc.root.contents()[0].getText()).to.equal('Alice h', '2');
1545
1546             doc.redo();
1547             expect(doc.root.contents()[0].getText()).to.equal('Alice ha', '3');
1548
1549             doc.redo();
1550             expect(doc.root.contents()[0].getText()).to.equal('Alice has', '4');
1551
1552             doc.undo();
1553             doc.undo();
1554             textNode = doc.getNodeByPath(path);
1555             textNode.setText('Cat');
1556             doc.undo();
1557             textNode = doc.getNodeByPath(path);
1558             expect(textNode.getText()).to.equal('Alice h');
1559         });
1560
1561         
1562         var sampleMethod = function(val) {
1563             this._$.attr('x', val);
1564             this.triggerChangeEvent();
1565         };
1566
1567         var transformations = {
1568             'unaware': sampleMethod,
1569             'returning change root': {
1570                 impl: sampleMethod,
1571                 getChangeRoot: function() {
1572                     return this.context;
1573                 }
1574             },
1575             'implementing undo operation': {
1576                 impl: function(t, val) {
1577                     t.oldVal = this.getAttr('x');
1578                     sampleMethod.call(this, val);
1579                 },
1580                 undo: function(t) {
1581                     this.setAttr('x', t.oldVal);
1582                 }
1583             }
1584         };
1585
1586         _.pairs(transformations).forEach(function(pair) {
1587             var name = pair[0],
1588                 transformaton = pair[1];
1589
1590             describe(name + ' transformation: ', function() {
1591                 var doc, node, nodePath;
1592
1593                 beforeEach(function() {
1594                     doc = getDocumentFromXML('<div><test x="old"></test></div>');
1595
1596                     doc.registerExtension({elementNode: {transformations: {
1597                         test: transformaton
1598                     }}});
1599
1600                     node = doc.root.contents()[0];
1601                     nodePath = node.getPath();
1602                 });
1603
1604                 it('transforms as expected', function() {
1605                     node.test('new');
1606                     expect(node.getAttr('x')).to.equal('new');
1607                 });
1608
1609                 it('can be undone', function() {
1610                     node.test('new');
1611                     doc.undo();
1612                     node = doc.getNodeByPath(nodePath);
1613                     expect(node.getAttr('x')).to.equal('old');
1614                 });
1615
1616                 it('can be undone and then redone', function() {
1617                     node.test('new');
1618                     doc.undo();
1619                     doc.redo();
1620                     node = doc.getNodeByPath(nodePath);
1621                     expect(node.getAttr('x')).to.equal('new');
1622                 });
1623
1624                 it('handles a sample scenario', function() {
1625                     doc.root.contents()[0].test('1');
1626                     doc.root.contents()[0].test('2');
1627                     doc.root.contents()[0].test('3');
1628                     doc.root.contents()[0].test('4');
1629                     doc.root.contents()[0].test('5');
1630
1631                     expect(doc.root.contents()[0].getAttr('x')).to.equal('5', 'after initial transformations');
1632                     doc.undo();
1633                     expect(doc.root.contents()[0].getAttr('x')).to.equal('4', 'undo 1.1');
1634                     doc.undo();
1635                     expect(doc.root.contents()[0].getAttr('x')).to.equal('3', 'undo 1.2');
1636                     doc.redo();
1637                     expect(doc.root.contents()[0].getAttr('x')).to.equal('4', 'redo 1.1');
1638                     doc.redo();
1639                     expect(doc.root.contents()[0].getAttr('x')).to.equal('5', 'redo 1.2');
1640                     doc.undo();
1641                     expect(doc.root.contents()[0].getAttr('x')).to.equal('4', 'undo 2.1');
1642                     doc.root.contents()[0].test('10');
1643                     expect(doc.root.contents()[0].getAttr('x')).to.equal('10', 'additional transformation');
1644                     expect(doc.redoStack.length).to.equal(0, 'transformation cleared redo stack');
1645                     doc.redo();
1646                     expect(doc.root.contents()[0].getAttr('x')).to.equal('10', 'empty redoStack so redo was noop');
1647                     doc.undo();
1648                     expect(doc.root.contents()[0].getAttr('x')).to.equal('4', 'undoing additional transformation');
1649                     doc.redo();
1650                     expect(doc.root.contents()[0].getAttr('x')).to.equal('10', 'redoing additional transformation');
1651                 });
1652             });
1653         });
1654
1655         it('smoke tests nested transformations', function() {
1656             var doc = getDocumentFromXML('<div></div>');
1657
1658             doc.registerExtension({elementNode: {transformations: {
1659                 nested: function(v) {
1660                     this._$.attr('innerAttr', v);
1661                     this.triggerChangeEvent();
1662                 },
1663                 outer: function(v) {
1664                     this.nested(v);
1665                     this._$.attr('outerAttr', v);
1666                     this.triggerChangeEvent();
1667                 }
1668             }}});
1669
1670             doc.root.outer('test1');
1671             doc.root.outer('test2');
1672
1673             expect(doc.root.getAttr('innerAttr')).to.equal('test2');
1674             expect(doc.root.getAttr('outerAttr')).to.equal('test2');
1675
1676             doc.undo();
1677
1678             expect(doc.root.getAttr('innerAttr')).to.equal('test1');
1679             expect(doc.root.getAttr('outerAttr')).to.equal('test1');
1680
1681             doc.undo();
1682
1683             expect(doc.root.getAttr('innerAttr')).to.equal(undefined);
1684             expect(doc.root.getAttr('outerAttr')).to.equal(undefined);
1685
1686             doc.redo();
1687
1688             expect(doc.root.getAttr('innerAttr')).to.equal('test1');
1689             expect(doc.root.getAttr('outerAttr')).to.equal('test1');
1690
1691             doc.redo();
1692
1693             expect(doc.root.getAttr('innerAttr')).to.equal('test2');
1694             expect(doc.root.getAttr('outerAttr')).to.equal('test2');
1695
1696         });
1697
1698         it('ignores transformation if document didn\'t emit change event', function() {
1699             var doc = getDocumentFromXML('<div></div>');
1700
1701             doc.registerExtension({elementNode: {transformations: {
1702                 test: function() {
1703                     // empty
1704                 }
1705             }}});
1706
1707             doc.root.test();
1708             expect(doc.undoStack.length).to.equal(0);
1709
1710         });
1711
1712         describe('Transactions', function() {
1713             it('allows to undo/redo series of transformations at once', function() {
1714                 var doc = getDocumentFromXML('<div></div>');
1715
1716                 doc.registerExtension({
1717                     elementNode: {transformations: {
1718                         test: function(v) {
1719                             this.setAttr('test', v);
1720                         }
1721                     }}
1722                 });
1723
1724                 doc.startTransaction();
1725                 doc.root.test('1');
1726                 doc.root.test('2');
1727                 doc.root.test('3');
1728                 doc.endTransaction();
1729
1730                 doc.undo();
1731                 expect(doc.root.getAttr('test'), '1');
1732                 doc.redo();
1733                 expect(doc.root.getAttr('test'), '3');
1734                 doc.undo();
1735                 expect(doc.root.getAttr('test'), '1');
1736                 doc.redo();
1737                 expect(doc.root.getAttr('test'), '3');
1738             });
1739
1740             it('ignores empty transactions', function() {
1741                 var doc = getDocumentFromXML('<div></div>');
1742                 doc.startTransaction();
1743                 doc.endTransaction();
1744                 expect(doc.undoStack).to.have.length(0, 'empty transaction doesn\'t get pushed into undo stack');
1745             });
1746
1747             it('doesn\'t break on optimizations', function() {
1748                 // This is a smoke test checking if optimizations made to transaction undoing
1749                 // doesnt't break anything.
1750                 var doc = getDocumentFromXML('<div smart="1" unaware="1"></div>');
1751
1752                 doc.registerExtension({
1753                     elementNode: {transformations: {
1754                         unaware: function(v) {
1755                             this.setAttr('unware', v);
1756                             this.triggerChangeEvent();
1757                         },
1758                         smart: {
1759                             impl: function(t, v) {
1760                                 t.oldVal = this.getAttr('smart');
1761                                 this.setAttr('smart', v);
1762                                 this.triggerChangeEvent();
1763                             },
1764                             undo: function(t) {
1765                                 this.setAttr('smart', t.oldVal);
1766                                 this.triggerChangeEvent();
1767                             }
1768                         }
1769                     }}
1770                 });
1771
1772                 doc.startTransaction();
1773                 doc.root.smart('2');
1774                 doc.root.unaware('2');
1775                 doc.root.smart('3');
1776                 doc.root.unaware('3');
1777                 doc.endTransaction();
1778
1779                 doc.undo();
1780
1781                 expect(doc.root.getAttr('smart')).to.equal('1');
1782                 expect(doc.root.getAttr('unaware')).to.equal('1');
1783             });
1784
1785             it('can have associated metadata', function() {
1786                 var doc = getDocumentFromXML('<div></div>'),
1787                     metadata = Object.create({});
1788
1789                 doc.registerExtension({document: {transformations: {
1790                     test: function() {
1791                         this.trigger('change');
1792                     }
1793                 }}});
1794
1795                 doc.startTransaction(metadata);
1796                 doc.test();
1797                 doc.endTransaction();
1798
1799                 var transaction = doc.undoStack[0];
1800                 expect(transaction.metadata).to.equal(metadata);
1801             });
1802
1803             it('can be rolled back', function() {
1804                 var doc = getDocumentFromXML('<root></root>');
1805
1806                 doc.startTransaction();
1807                 doc.root.append({tagName: 'div'});
1808                 doc.rollbackTransaction();
1809
1810                 expect(doc.undoStack.length).to.equal(0, 'nothing to undo');
1811                 expect(doc.root.contents().length).to.equal(0);
1812             });
1813
1814             it('rollbacks and calls error handleor if error gets thrown', function() {
1815                 var doc = getDocumentFromXML('<root></root>'),
1816                     err = new Error(),
1817                     spy = sinon.spy();
1818                 
1819                 doc.transaction(function() {
1820                     doc.root.append({tagName: 'div'});
1821                     throw err;
1822                 }, {error: spy});
1823
1824                 expect(spy.args[0][0]).to.equal(err);
1825                 expect(doc.root.contents().length).to.equal(0);
1826                 expect(doc.undoStack.length).to.equal(0);
1827             });
1828         });
1829
1830         describe('Regression tests', function() {
1831             it('redos correctly after running its own undo followed by unaware transformation undo', function() {
1832                 var doc = getDocumentFromXML('<section t="0"></section>');
1833                 
1834                 doc.registerExtension({elementNode: {transformations: {
1835                     unaware: function() {
1836                         this.triggerChangeEvent();
1837                     },
1838                     test: {
1839                         impl: function() {
1840                             this._$.attr('t', 1);
1841                             this.triggerChangeEvent();
1842                         },
1843                         undo: function() {
1844                             this._$.attr('t', 0);
1845                         }
1846                     }
1847                 }}});
1848                 doc.root.unaware();
1849                 doc.root.test();
1850                 doc.undo();
1851                 doc.undo();
1852                 doc.redo();
1853                 doc.redo();
1854                 expect(doc.root.getAttr('t')).to.equal('1');
1855             });
1856             it('can perform undo of an operation performed after automatic transaction rollback', function() {
1857                 var doc = getDocumentFromXML('<section></section>'),
1858                     extension = {document: {transformations: {
1859                         throwingTransformation: function() { throw new Error(); }
1860                     }}};
1861
1862                 doc.registerExtension(extension);
1863
1864                 doc.throwingTransformation();
1865
1866                 doc.transaction(function() {
1867                     doc.root.setAttr('x', '2');
1868                 });
1869
1870                 expect(doc.undoStack.length).to.equal(1);
1871                 expect(doc.root.getAttr('x')).to.equal('2');
1872
1873                 doc.undo();
1874
1875                 expect(doc.undoStack.length).to.equal(0);
1876                 expect(doc.root.getAttr('x')).to.be.undefined;
1877
1878             });
1879         });
1880     });
1881
1882 });
1883
1884 });