93c401ba6da43f21feaf28393b24a2c097783eed
[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                     aliceText = section.contents()[0],
772                     x = section.contents()[1],
773                     lastDiv = section.contents()[2];
774
775                 section.document.registerExtension({documentNode: {methods: {
776                     object: {
777                         describesParent: function() {
778                             return this.nodeType === Node.ELEMENT_NODE && this.getTagName() === 'x';
779                         }
780                     }
781                 }}});
782
783                 section.document.wrapNodes({
784                         node1: aliceText,
785                         node2: lastDiv,
786                         _with: {tagName: 'header'}
787                     });
788
789                 expect(x.parent().sameNode(section)).to.be.true;
790                 expect(aliceText.parent().getTagName()).to.equal('header');
791                 expect(lastDiv.parent().getTagName()).to.equal('header');
792             });
793         });
794
795     });
796
797     var getTextNodes = function(text, doc) {
798         /* globals Node */
799         var toret = [];
800         var search = function(node) {
801             node.contents().forEach(function(node) {
802                 if(node.nodeType === Node.TEXT_NODE) {
803                     if(node.getText() === text) {
804                         toret.push(node);
805                     }
806                 } else {
807                     search(node);
808                 }
809             });
810         };
811         search(doc.root);
812         return toret;
813     };
814
815     var getTextNode = function(text, doc) {
816         var nodes = getTextNodes(text, doc),
817             error;
818         if(nodes.length === 0) {
819             error = 'Text not found';
820         } else if(nodes.length > 1) {
821             error = 'Text not unique';
822         } else if(nodes[0].getText() !== text) {
823             error = 'I was trying to cheat your test :(';
824         }
825         if(error) {
826             throw new Error(error);
827         }
828         return nodes[0];
829     };
830
831     describe('Removing arbitrary text', function() {
832         it('removes within single text element', function() {
833             var doc = getDocumentFromXML('<div>Alice</div>'),
834                 text = getTextNode('Alice', doc);
835             doc.deleteText({
836                 from: {
837                     node: text,
838                     offset: 1
839                 },
840                 to: {
841                     node: text,
842                     offset: 4
843                 }
844             });
845             expect(doc.root.contents().length).to.equal(1);
846             expect(doc.root.contents()[0].getText()).to.equal('Ae');
847         });
848         it('removes across elements - 1', function() {
849             var doc = getDocumentFromXML('<div><a>aaa</a><b>bbb</b></div>');
850
851             doc.deleteText({
852                 from: {
853                     node: getTextNode('aaa', doc),
854                     offset: 2
855                 },
856                 to: {
857                     node: getTextNode('bbb', doc),
858                     offset: 2
859                 }
860             });
861
862             var contents = doc.root.contents();
863             expect(contents.length).to.equal(2);
864             expect(contents[0].contents()[0].getText()).to.equal('aa');
865             expect(contents[1].contents()[0].getText()).to.equal('b');
866         });
867         it('removes across elements - 2', function() {
868             var doc = getDocumentFromXML('<a><b><c>ccc</c></b>xxx</a>');
869             doc.deleteText({
870                 from: {
871                     node: getTextNode('ccc', doc),
872                     offset: 2
873                 },
874                 to: {
875                     node: getTextNode('xxx', doc),
876                     offset: 2
877                 }
878             });
879
880             var contents = doc.root.contents();
881             expect(contents.length).to.equal(2);
882             expect(contents[0].getTagName()).to.equal('b');
883             expect(contents[1].getText()).to.equal('x');
884             
885             var bContents = contents[0].contents();
886             expect(bContents.length).to.equal(1);
887             expect(bContents[0].getTagName()).to.equal('c');
888             expect(bContents[0].contents().length).to.equal(1);
889             expect(bContents[0].contents()[0].getText()).to.equal('cc');
890         });
891         it('remove across elements - 3 (merged text nodes)', function() {
892             var doc = getDocumentFromXML('<div>Alice <span>has</span> a cat</div>');
893             doc.deleteText({
894                 from: {
895                     node: getTextNode('Alice ', doc),
896                     offset: 1
897                 },
898                 to: {
899                     node: getTextNode(' a cat', doc),
900                     offset: 3
901                 }
902             });
903             var contents = doc.root.contents();
904             expect(contents.length).to.equal(1);
905             expect(contents[0].getText()).to.equal('Acat');
906         });
907         it('remove across elements - 4', function() {
908             var doc = getDocumentFromXML('<div>Alice <div>has <span>a</span> cat</div></div>');
909             doc.deleteText({
910                 from: {
911                     node: getTextNode('Alice ', doc),
912                     offset: 1
913                 },
914                 to: {
915                     node: getTextNode(' cat', doc),
916                     offset: 1
917                 }
918             });
919             var contents = doc.root.contents();
920             expect(contents.length).to.equal(2);
921             expect(contents[0].getText()).to.equal('A');
922             expect(contents[1].getTagName()).to.equal('div');
923             expect(contents[1].contents().length).to.equal(1);
924             expect(contents[1].contents()[0].getText()).to.equal('cat');
925         });
926         it('removes across elements - 5 (whole document)', function() {
927             var doc = getDocumentFromXML('<div>Alice <div>has <span>a</span> cat</div>!!!</div>');
928             doc.deleteText({
929                 from: {
930                     node: getTextNode('Alice ', doc),
931                     offset: 0
932                 },
933                 to: {
934                     node: getTextNode('!!!', doc),
935                     offset: 3
936                 }
937             });
938
939             expect(doc.root.getTagName()).to.equal('div');
940             expect(doc.root.contents().length).to.equal(1);
941             expect(doc.root.contents()[0].getText()).to.equal('');
942         });
943         it('removes nodes in between', function() {
944             var doc = getDocumentFromXML('<div><a>aaa<x>!</x></a>xxx<x></x><b><x>!</x>bbb</b></div>');
945             doc.deleteText({
946                 from: {
947                     node: getTextNode('aaa', doc),
948                     offset: 2
949                 },
950                 to: {
951                     node: getTextNode('bbb', doc),
952                     offset: 2
953                 }
954             });
955
956             var contents = doc.root.contents();
957             expect(contents.length).to.equal(2, 'two nodes survived');
958             expect(contents[0].getTagName()).to.equal('a');
959             expect(contents[1].getTagName()).to.equal('b');
960             expect(contents[0].contents().length).to.equal(1);
961             expect(contents[0].contents()[0].getText()).to.equal('aa');
962             expect(contents[1].contents().length).to.equal(1);
963             expect(contents[1].contents()[0].getText()).to.equal('b');
964         });
965         it('removes across elements - 6', function() {
966             var doc = getDocumentFromXML('<root><div>aaa<span>bbb</span>ccc</div><div>ddd</div></root>');
967             doc.deleteText({
968                 from: {
969                     node: getTextNode('aaa', doc),
970                     offset: 1
971                 },
972                 to: {
973                     node: getTextNode('ddd', doc),
974                     offset: 1
975                 }
976             }, {
977                 error: function(e) {throw e;}
978             });
979
980             var contents = doc.root.contents();
981             expect(contents.length).to.equal(2);
982             expect(contents[0].contents().length).to.equal(1);
983             expect(contents[0].contents()[0].getText()).to.equal('a');
984             expect(contents[1].contents().length).to.equal(1);
985             expect(contents[1].contents()[0].getText()).to.equal('dd');
986         });
987     });
988
989     describe('Splitting text', function() {
990     
991         it('splits TextNode\'s parent into two ElementNodes', function() {
992             var doc = getDocumentFromXML('<section><header>Some header</header></section>'),
993                 section = doc.root,
994                 text = section.contents()[0].contents()[0];
995
996             var returnedValue = text.split({offset: 5});
997             expect(section.contents().length).to.equal(2, 'section has two children');
998             
999             var header1 = section.contents()[0];
1000             var header2 = section.contents()[1];
1001
1002             expect(header1.getTagName()).to.equal('header', 'first section child ok');
1003             expect(header1.contents().length).to.equal(1, 'first header has one child');
1004             expect(header1.contents()[0].getText()).to.equal('Some ', 'first header has correct content');
1005             expect(header2.getTagName()).to.equal('header', 'second section child ok');
1006             expect(header2.contents().length).to.equal(1, 'second header has one child');
1007             expect(header2.contents()[0].getText()).to.equal('header', 'second header has correct content');
1008
1009             expect(returnedValue.first.sameNode(header1)).to.equal(true, 'first node returned');
1010             expect(returnedValue.second.sameNode(header2)).to.equal(true, 'second node returned');
1011         });
1012
1013         it('leaves empty copy of ElementNode if splitting at the very beginning', function() {
1014                 var doc = getDocumentFromXML('<section><header>Some header</header></section>'),
1015                 section = doc.root,
1016                 text = section.contents()[0].contents()[0];
1017
1018                 text.split({offset: 0});
1019                 
1020                 var header1 = section.contents()[0];
1021                 var header2 = section.contents()[1];
1022
1023                 expect(header1.contents().length).to.equal(0);
1024                 expect(header2.contents()[0].getText()).to.equal('Some header');
1025         });
1026
1027         it('leaves empty copy of ElementNode if splitting at the very end', function() {
1028                 var doc = getDocumentFromXML('<section><header>Some header</header></section>'),
1029                 section = doc.root,
1030                 text = section.contents()[0].contents()[0];
1031
1032                 text.split({offset: 11});
1033                 
1034                 var header1 = section.contents()[0];
1035                 var header2 = section.contents()[1];
1036
1037                 expect(header1.contents()[0].getText()).to.equal('Some header');
1038                 expect(header2.contents().length).to.equal(0);
1039         });
1040
1041         it('keeps TextNodes\'s parent\'s children elements intact', function() {
1042             var doc = getDocumentFromXML('<section><header>A <span>fancy</span> and <span>nice</span> header</header></section>'),
1043                 section = doc.root,
1044                 header = section.contents()[0],
1045                 textAnd = header.contents()[2];
1046
1047             textAnd.split({offset: 2});
1048             
1049             var sectionContents = section.contents();
1050             expect(sectionContents.length).to.equal(2, 'Section has two children');
1051             expect(sectionContents[0].getTagName()).to.equal('header', 'First section node is a header');
1052             expect(sectionContents[1].getTagName()).to.equal('header', 'Second section node is a header');
1053
1054             var firstHeaderContents = sectionContents[0].contents();
1055             expect(firstHeaderContents.length).to.equal(3, 'First header has three children');
1056             expect(firstHeaderContents[0].getText()).to.equal('A ', 'First header starts with a text');
1057             expect(firstHeaderContents[1].getTagName()).to.equal('span', 'First header has span in the middle');
1058             expect(firstHeaderContents[2].getText()).to.equal(' a', 'First header ends with text');
1059
1060             var secondHeaderContents = sectionContents[1].contents();
1061             expect(secondHeaderContents.length).to.equal(3, 'Second header has three children');
1062             expect(secondHeaderContents[0].getText()).to.equal('nd ', 'Second header starts with text');
1063             expect(secondHeaderContents[1].getTagName()).to.equal('span', 'Second header has span in the middle');
1064             expect(secondHeaderContents[2].getText()).to.equal(' header', 'Second header ends with text');
1065         });
1066     });
1067
1068     describe('Events', function() {
1069         it('emits nodeDetached event on node detach', function() {
1070             var node = elementNodeFromXML('<div><div></div></div>'),
1071                 innerNode = node.contents()[0],
1072                 spy = sinon.spy();
1073             node.document.on('change', spy);
1074             
1075             var detached = innerNode.detach(),
1076                 event = spy.args[0][0];
1077
1078             expect(event.type).to.equal('nodeDetached');
1079             expect(event.meta.node.sameNode(detached, 'detached node in event meta'));
1080             expect(event.meta.parent.sameNode(node), 'original parent node in event meta');
1081         }),
1082
1083         it('emits nodeAdded event when appending new node', function() {
1084             var node = elementNodeFromXML('<div></div>'),
1085                 spy = sinon.spy();
1086             node.document.on('change', spy);
1087             
1088             var appended = node.append({tagName:'div'}),
1089                 event = spy.args[0][0];
1090             expect(event.type).to.equal('nodeAdded');
1091             expect(event.meta.node.sameNode(appended)).to.be.true;
1092         });
1093         
1094         it('emits nodeDetached/nodeAdded events with `move` flag when appending aready existing node', function() {
1095             var node = elementNodeFromXML('<div><a></a><b></b></div>'),
1096                 a = node.contents()[0],
1097                 b = node.contents()[1],
1098                 spy = sinon.spy();
1099             node.document.on('change', spy);
1100             
1101             var appended = a.append(b),
1102                 detachedEvent = spy.args[0][0],
1103                 addedEvent = spy.args[1][0];
1104
1105             expect(spy.callCount).to.equal(2);
1106             expect(detachedEvent.type).to.equal('nodeDetached');
1107             expect(detachedEvent.meta.node.sameNode(appended)).to.be.true;
1108             expect(detachedEvent.meta.move).to.equal(true, 'move flag set to true for nodeDetachedEvent');
1109             expect(addedEvent.type).to.equal('nodeAdded');
1110             expect(addedEvent.meta.node.sameNode(appended)).to.be.true;
1111             expect(addedEvent.meta.move).to.equal(true, 'move flag set to true for nodeAddedEvent');
1112
1113         });
1114         
1115         it('emits nodeAdded event when prepending new node', function() {
1116             var node = elementNodeFromXML('<div></div>'),
1117                 spy = sinon.spy();
1118             node.document.on('change', spy);
1119             
1120             var prepended = node.prepend({tagName:'div'}),
1121                 event = spy.args[0][0];
1122             expect(event.type).to.equal('nodeAdded');
1123             expect(event.meta.node.sameNode(prepended)).to.be.true;
1124         });
1125         
1126         it('emits nodeDetached/nodeAdded events with `move` flag when prepending aready existing node', function() {
1127             var node = elementNodeFromXML('<div><a></a><b></b></div>'),
1128                 a = node.contents()[0],
1129                 b = node.contents()[1],
1130                 spy = sinon.spy();
1131             node.document.on('change', spy);
1132
1133             var prepended = a.prepend(b),
1134                 detachedEvent = spy.args[0][0],
1135                 addedEvent = spy.args[1][0];
1136
1137             expect(spy.callCount).to.equal(2);
1138             expect(detachedEvent.type).to.equal('nodeDetached');
1139             expect(detachedEvent.meta.node.sameNode(prepended)).to.be.true;
1140             expect(detachedEvent.meta.move).to.equal(true, 'move flag set to true for nodeDetachedEvent');
1141             expect(addedEvent.type).to.equal('nodeAdded');
1142             expect(addedEvent.meta.node.sameNode(prepended)).to.be.true;
1143             expect(addedEvent.meta.move).to.equal(true, 'move flag set to true for nodeAddedEvent');
1144         });
1145         
1146         it('emits nodeAdded event when inserting node after another', function() {
1147             var node = elementNodeFromXML('<div><a></a></div>').contents()[0],
1148                 spy = sinon.spy();
1149             node.document.on('change', spy);
1150             
1151             var inserted = node.after({tagName:'div'}),
1152                 event = spy.args[0][0];
1153             expect(event.type).to.equal('nodeAdded');
1154             expect(event.meta.node.sameNode(inserted)).to.be.true;
1155         });
1156         
1157         it('emits nodeDetached/nodeAdded events with `move` flag when inserting aready existing node after another', function() {
1158             var node = elementNodeFromXML('<div><a></a><b></b></div>'),
1159                 a = node.contents()[0],
1160                 b = node.contents()[1],
1161                 spy = sinon.spy();
1162             node.document.on('change', spy);
1163             var inserted = b.after(a),
1164                 detachedEvent = spy.args[0][0],
1165                 addedEvent = spy.args[1][0];
1166
1167             expect(spy.callCount).to.equal(2);
1168             expect(detachedEvent.type).to.equal('nodeDetached');
1169             expect(detachedEvent.meta.node.sameNode(inserted)).to.be.true;
1170             expect(detachedEvent.meta.move).to.equal(true, 'move flag set to true for nodeDetachedEvent');
1171             expect(addedEvent.type).to.equal('nodeAdded');
1172             expect(addedEvent.meta.node.sameNode(inserted)).to.be.true;
1173             expect(addedEvent.meta.move).to.equal(true, 'move flag set to true for nodeAddedEvent');
1174         });
1175
1176         it('emits nodeAdded event when inserting node before another', function() {
1177             var node = elementNodeFromXML('<div><a></a></div>').contents()[0],
1178                 spy = sinon.spy();
1179             node.document.on('change', spy);
1180             
1181             var inserted = node.before({tagName:'div'}),
1182                 event = spy.args[0][0];
1183             expect(event.type).to.equal('nodeAdded');
1184             expect(event.meta.node.sameNode(inserted)).to.be.true;
1185         });
1186         
1187         it('emits nodeDetached/nodeAdded events with `move` flag when inserting aready existing node before another', function() {
1188             var node = elementNodeFromXML('<div><a></a><b></b></div>'),
1189                 a = node.contents()[0],
1190                 b = node.contents()[1],
1191                 spy = sinon.spy();
1192             node.document.on('change', spy);
1193             var inserted = a.before(b),
1194                 detachedEvent = spy.args[0][0],
1195                 addedEvent = spy.args[1][0];
1196
1197             expect(spy.callCount).to.equal(2);
1198             expect(detachedEvent.type).to.equal('nodeDetached');
1199             expect(detachedEvent.meta.node.sameNode(inserted)).to.be.true;
1200             expect(detachedEvent.meta.move).to.equal(true, 'move flag set to true for nodeDetachedEvent');
1201             expect(addedEvent.type).to.equal('nodeAdded');
1202             expect(addedEvent.meta.node.sameNode(inserted)).to.be.true;
1203             expect(addedEvent.meta.move).to.equal(true, 'move flag set to true for nodeAddedEvent');
1204         });
1205
1206         it('emits nodeDetached and nodeAdded when replacing root node with another', function() {
1207             var doc = getDocumentFromXML('<a></a>'),
1208                 oldRoot = doc.root,
1209                 spy = sinon.spy();
1210
1211             doc.on('change', spy);
1212
1213             doc.root.replaceWith({tagName: 'b'});
1214
1215             expect(spy.callCount).to.equal(2);
1216
1217             var event1 = spy.args[0][0],
1218                 event2 = spy.args[1][0];
1219
1220             expect(event1.type).to.equal('nodeDetached');
1221             expect(event1.meta.node.sameNode(oldRoot)).to.equal(true, 'root node in nodeDetached event metadata');
1222             expect(event2.type).to.equal('nodeAdded');
1223             expect(event2.meta.node.sameNode(doc.root)).to.equal(true, 'new root node in nodelAdded event meta');
1224         });
1225
1226
1227         ['append', 'prepend', 'before', 'after'].forEach(function(insertionMethod) {
1228             it('emits nodeDetached for node moved from a document tree to out of document node ' + insertionMethod, function() {
1229                 var doc = getDocumentFromXML('<div><a></a></div>'),
1230                     a = doc.root.contents()[0],
1231                     spy = sinon.spy();
1232
1233                 doc.on('change', spy);
1234
1235                 var newNode = doc.createDocumentNode({tagName: 'b'}),
1236                     newNodeInner = newNode.append({tagName:'c'});
1237
1238                 newNodeInner[insertionMethod](a);
1239
1240                 var event = spy.args[0][0];
1241                 expect(event.type).to.equal('nodeDetached');
1242                 expect(event.meta.node.sameNode(a));
1243             });
1244
1245             it('doesn\'t emit nodeDetached event for already out of document node moved to out of document node' + insertionMethod, function() {
1246                 var doc = getDocumentFromXML('<div><a></a></div>'),
1247                     spy = sinon.spy();
1248
1249                 doc.on('change', spy);
1250
1251                 var newNode = doc.createDocumentNode({tagName: 'b'});
1252                 newNode.append({tagName:'c'});
1253
1254                 expect(spy.callCount).to.equal(0);
1255             });
1256         });
1257
1258
1259     });
1260
1261     describe('Traversing', function() {
1262         describe('Basic', function() {
1263             it('can access node parent', function() {
1264                 var doc = getDocumentFromXML('<a><b></b></a>'),
1265                     a = doc.root,
1266                     b = a.contents()[0];
1267
1268                 expect(a.parent()).to.equal(null, 'parent of a root is null');
1269                 expect(b.parent().sameNode(a)).to.be.true;
1270             });
1271             it('can access node parents', function() {
1272                 var doc = getDocumentFromXML('<a><b><c></c></b></a>'),
1273                     a = doc.root,
1274                     b = a.contents()[0],
1275                     c = b.contents()[0];
1276
1277                 var parents = c.parents();
1278                 // @@
1279                 expect(parents[0].sameNode(b)).to.be.true;
1280                 expect(parents[1].sameNode(a)).to.be.true;
1281             });
1282         });
1283
1284         describe('finding sibling parents of two elements', function() {
1285             it('returns elements themself if they have direct common parent', function() {
1286                 var doc = getDocumentFromXML('<section><div><div>A</div><div>B</div></div></section>'),
1287                     wrappingDiv = doc.root.contents()[0],
1288                     divA = wrappingDiv.contents()[0],
1289                     divB = wrappingDiv.contents()[1];
1290
1291                 var siblingParents = doc.getSiblingParents({node1: divA, node2: divB});
1292
1293                 expect(siblingParents.node1.sameNode(divA)).to.equal(true, 'divA');
1294                 expect(siblingParents.node2.sameNode(divB)).to.equal(true, 'divB');
1295             });
1296
1297             it('returns sibling parents - example 1', function() {
1298                 var doc = getDocumentFromXML('<section>Alice <span>has a cat</span></section>'),
1299                     aliceText = doc.root.contents()[0],
1300                     span = doc.root.contents()[1],
1301                     spanText = span.contents()[0];
1302
1303                 var siblingParents = doc.getSiblingParents({node1: aliceText, node2: spanText});
1304
1305                 expect(siblingParents.node1.sameNode(aliceText)).to.equal(true, 'aliceText');
1306                 expect(siblingParents.node2.sameNode(span)).to.equal(true, 'span');
1307             });
1308
1309             it('returns node itself for two same nodes', function() {
1310                 var doc = getDocumentFromXML('<section><div></div></section>'),
1311                     div = doc.root.contents()[0];
1312
1313                 var siblingParents = doc.getSiblingParents({node1: div, node2: div});
1314                 expect(!!siblingParents.node1 && !!siblingParents.node2).to.equal(true, 'nodes defined');
1315                 expect(siblingParents.node1.sameNode(div)).to.be.equal(true, 'node1');
1316                 expect(siblingParents.node2.sameNode(div)).to.be.equal(true, 'node2');
1317             });
1318         });
1319     });
1320
1321     describe('Serializing document to WLXML', function() {
1322         it('keeps document intact when no changes have been made', function() {
1323             var xmlIn = '<section>Alice<div>has</div>a <span class="uri" meta-uri="http://cat.com">cat</span>!</section>',
1324                 doc = getDocumentFromXML(xmlIn),
1325                 xmlOut = doc.toXML();
1326
1327             var parser = new DOMParser(),
1328                 input = parser.parseFromString(xmlIn, 'application/xml').childNodes[0],
1329                 output = parser.parseFromString(xmlOut, 'application/xml').childNodes[0];
1330             
1331             expect(input.isEqualNode(output)).to.be.true;
1332         });
1333
1334         it('keeps entities intact', function() {
1335             var xmlIn = '<section>&lt; &gt;</section>',
1336                 doc = getDocumentFromXML(xmlIn),
1337                 xmlOut = doc.toXML();
1338             expect(xmlOut).to.equal(xmlIn);
1339         });
1340         it('keeps entities intact when they form html/xml', function() {
1341             var xmlIn = '<section>&lt;abc&gt;</section>',
1342                 doc = getDocumentFromXML(xmlIn),
1343                 xmlOut = doc.toXML();
1344             expect(xmlOut).to.equal(xmlIn);
1345         });
1346     });
1347
1348     describe('Extension API', function() {
1349         var doc, extension, elementNode, textNode;
1350
1351         beforeEach(function() {
1352             doc = getDocumentFromXML('<section>Alice<div class="test_class"></div></section>');
1353         });
1354
1355         it('allows adding method to a document', function() {
1356             extension = {document: {methods: {
1357                 testMethod: function() { return this; }
1358             }}};
1359
1360             doc.registerExtension(extension);
1361             expect(doc.testMethod()).to.equal(doc, 'context is set to a document instance');
1362         });
1363
1364         it('allows adding transformation to a document', function() {
1365             extension = {document: {transformations: {
1366                 testTransformation: function() { return this; },
1367                 testTransformation2: {impl: function() { return this;}}
1368             }}};
1369
1370             doc.registerExtension(extension);
1371             expect(doc.testTransformation()).to.equal(doc, 'context is set to a document instance');
1372             expect(doc.testTransformation2()).to.equal(doc, 'context is set to a document instance');
1373         });
1374
1375         it('allows adding method to a DocumentNode instance', function() {
1376             extension = {
1377                 documentNode: {
1378                     methods: {
1379                         testMethod: function() { return this; }
1380                     }
1381                 },
1382                 textNode: {
1383                     methods: {
1384                         textTestMethod: function() { return this; }
1385                     }
1386                 },
1387                 elementNode: {
1388                     methods: {
1389                         elementTestMethod: function() { return this; }
1390                     }
1391                 }
1392             };
1393
1394             doc.registerExtension(extension);
1395
1396             elementNode = doc.root;
1397             textNode = doc.root.contents()[0];
1398
1399             expect(elementNode.testMethod().sameNode(elementNode)).to.equal(true, 'context is set to a node instance');
1400             expect(textNode.testMethod().sameNode(textNode)).to.equal(true, 'context is set to a node instance');
1401
1402             expect(elementNode.elementTestMethod().sameNode(elementNode)).to.be.true;
1403             expect(elementNode.textTestMethod).to.be.undefined;
1404         
1405             expect(textNode.textTestMethod().sameNode(textNode)).to.be.true;
1406             expect(textNode.elementTestMethod).to.be.undefined;
1407         });
1408
1409         it('allows adding transformation to a DocumentNode', function() {
1410             extension = {
1411                 documentNode: {
1412                     transformations: {
1413                         testTransformation: function() { return this; },
1414                         testTransformation2: {impl: function() { return this;}}
1415                     }
1416                 },
1417                 textNode: {
1418                     transformations: {
1419                         textTestTransformation: function() { return this; }
1420                     }
1421                 },
1422                 elementNode: {
1423                     transformations: {
1424                         elementTestTransformation: function() { return this; }
1425                     }
1426                 }
1427             };
1428             
1429             doc.registerExtension(extension);
1430
1431             elementNode = doc.root;
1432             textNode = doc.root.contents()[0];
1433             
1434             expect(elementNode.testTransformation().sameNode(elementNode)).to.equal(true, '1');
1435             expect(elementNode.testTransformation2().sameNode(elementNode)).to.equal(true, '2');
1436             expect(textNode.testTransformation().sameNode(textNode)).to.equal(true, '3');
1437             expect(textNode.testTransformation2().sameNode(textNode)).to.equal(true, '4');
1438
1439             expect(elementNode.elementTestTransformation().sameNode(elementNode)).to.be.true;
1440             expect(elementNode.textTestTransformation).to.be.undefined;
1441         
1442             expect(textNode.textTestTransformation().sameNode(textNode)).to.be.true;
1443             expect(textNode.elementTestTransfomation).to.be.undefined;
1444         });
1445
1446         it('allows text/element node methods and transformations to access node and transormations on document node', function() {
1447
1448             var doc = getDocumentFromXML('<div>text</div>');
1449
1450             doc.registerExtension({
1451                 documentNode: {
1452                     methods: {
1453                         test: function() {
1454                             return 'super';
1455                         }
1456                     },
1457                     transformations: {
1458                         testT: function() {
1459                             return 'super_trans';
1460                         }
1461                     }
1462                 },
1463                 elementNode: {
1464                     methods: {
1465                         test: function() {
1466                             return 'element_sub_' + this.__super__.test();
1467                         }
1468                     },
1469                     transformations: {
1470                         testT: function() {
1471                             return 'element_trans_sub_' + this.__super__.testT();
1472                         }
1473                     }
1474                 },
1475                 textNode: {
1476                     methods: {
1477                         test: function() {
1478                             return 'text_sub_' + this.__super__.test();
1479                         }
1480                     },
1481                     transformations: {
1482                         testT: function() {
1483                             return 'text_trans_sub_' + this.__super__.testT();
1484                         }
1485                     }
1486                 }
1487             });
1488
1489             var textNode = doc.root.contents()[0];
1490
1491             expect(doc.root.test()).to.equal('element_sub_super');
1492             expect(textNode.test()).to.equal('text_sub_super');
1493             expect(doc.root.testT()).to.equal('element_trans_sub_super_trans');
1494             expect(textNode.testT()).to.equal('text_trans_sub_super_trans');
1495         });
1496     });
1497
1498     describe('Undo/redo', function() {
1499
1500         it('smoke tests', function() {
1501             var doc = getDocumentFromXML('<div>Alice</div>'),
1502                 textNode = doc.root.contents()[0];
1503
1504             expect(doc.undoStack).to.have.length(0);
1505             
1506             textNode.wrapWith({tagName: 'span', start:1, end:2});
1507             expect(doc.undoStack).to.have.length(1, '1');
1508             expect(doc.toXML()).to.equal('<div>A<span>l</span>ice</div>');
1509
1510             doc.undo();
1511             expect(doc.undoStack).to.have.length(0, '2');
1512             expect(doc.toXML()).to.equal('<div>Alice</div>');
1513
1514             doc.redo();
1515             expect(doc.undoStack).to.have.length(1, '3');
1516             expect(doc.toXML()).to.equal('<div>A<span>l</span>ice</div>');
1517
1518             doc.undo();
1519             expect(doc.undoStack).to.have.length(0, '4');
1520             expect(doc.toXML()).to.equal('<div>Alice</div>');
1521
1522             doc.undo();
1523             expect(doc.undoStack).to.have.length(0, '5');
1524             expect(doc.toXML()).to.equal('<div>Alice</div>');
1525         });
1526
1527         it('smoke tests 2', function() {
1528             var doc = getDocumentFromXML('<div>Alice</div>'),
1529                 textNode = doc.root.contents()[0],
1530                 path = textNode.getPath();
1531
1532             textNode.setText('Alice ');
1533             textNode.setText('Alice h');
1534             textNode.setText('Alice ha');
1535             textNode.setText('Alice has');
1536
1537             expect(textNode.getText()).to.equal('Alice has');
1538
1539             doc.undo();
1540             expect(doc.root.contents()[0].getText()).to.equal('Alice ha', '1');
1541
1542             doc.undo();
1543             expect(doc.root.contents()[0].getText()).to.equal('Alice h', '2');
1544
1545             doc.redo();
1546             expect(doc.root.contents()[0].getText()).to.equal('Alice ha', '3');
1547
1548             doc.redo();
1549             expect(doc.root.contents()[0].getText()).to.equal('Alice has', '4');
1550
1551             doc.undo();
1552             doc.undo();
1553             textNode = doc.getNodeByPath(path);
1554             textNode.setText('Cat');
1555             doc.undo();
1556             textNode = doc.getNodeByPath(path);
1557             expect(textNode.getText()).to.equal('Alice h');
1558         });
1559
1560         
1561         var sampleMethod = function(val) {
1562             this._$.attr('x', val);
1563             this.triggerChangeEvent();
1564         };
1565
1566         var transformations = {
1567             'unaware': sampleMethod,
1568             'returning change root': {
1569                 impl: sampleMethod,
1570                 getChangeRoot: function() {
1571                     return this.context;
1572                 }
1573             },
1574             'implementing undo operation': {
1575                 impl: function(t, val) {
1576                     t.oldVal = this.getAttr('x');
1577                     sampleMethod.call(this, val);
1578                 },
1579                 undo: function(t) {
1580                     this.setAttr('x', t.oldVal);
1581                 }
1582             }
1583         };
1584
1585         _.pairs(transformations).forEach(function(pair) {
1586             var name = pair[0],
1587                 transformaton = pair[1];
1588
1589             describe(name + ' transformation: ', function() {
1590                 var doc, node, nodePath;
1591
1592                 beforeEach(function() {
1593                     doc = getDocumentFromXML('<div><test x="old"></test></div>');
1594
1595                     doc.registerExtension({elementNode: {transformations: {
1596                         test: transformaton
1597                     }}});
1598
1599                     node = doc.root.contents()[0];
1600                     nodePath = node.getPath();
1601                 });
1602
1603                 it('transforms as expected', function() {
1604                     node.test('new');
1605                     expect(node.getAttr('x')).to.equal('new');
1606                 });
1607
1608                 it('can be undone', function() {
1609                     node.test('new');
1610                     doc.undo();
1611                     node = doc.getNodeByPath(nodePath);
1612                     expect(node.getAttr('x')).to.equal('old');
1613                 });
1614
1615                 it('can be undone and then redone', function() {
1616                     node.test('new');
1617                     doc.undo();
1618                     doc.redo();
1619                     node = doc.getNodeByPath(nodePath);
1620                     expect(node.getAttr('x')).to.equal('new');
1621                 });
1622
1623                 it('handles a sample scenario', function() {
1624                     doc.root.contents()[0].test('1');
1625                     doc.root.contents()[0].test('2');
1626                     doc.root.contents()[0].test('3');
1627                     doc.root.contents()[0].test('4');
1628                     doc.root.contents()[0].test('5');
1629
1630                     expect(doc.root.contents()[0].getAttr('x')).to.equal('5', 'after initial transformations');
1631                     doc.undo();
1632                     expect(doc.root.contents()[0].getAttr('x')).to.equal('4', 'undo 1.1');
1633                     doc.undo();
1634                     expect(doc.root.contents()[0].getAttr('x')).to.equal('3', 'undo 1.2');
1635                     doc.redo();
1636                     expect(doc.root.contents()[0].getAttr('x')).to.equal('4', 'redo 1.1');
1637                     doc.redo();
1638                     expect(doc.root.contents()[0].getAttr('x')).to.equal('5', 'redo 1.2');
1639                     doc.undo();
1640                     expect(doc.root.contents()[0].getAttr('x')).to.equal('4', 'undo 2.1');
1641                     doc.root.contents()[0].test('10');
1642                     expect(doc.root.contents()[0].getAttr('x')).to.equal('10', 'additional transformation');
1643                     expect(doc.redoStack.length).to.equal(0, 'transformation cleared redo stack');
1644                     doc.redo();
1645                     expect(doc.root.contents()[0].getAttr('x')).to.equal('10', 'empty redoStack so redo was noop');
1646                     doc.undo();
1647                     expect(doc.root.contents()[0].getAttr('x')).to.equal('4', 'undoing additional transformation');
1648                     doc.redo();
1649                     expect(doc.root.contents()[0].getAttr('x')).to.equal('10', 'redoing additional transformation');
1650                 });
1651             });
1652         });
1653
1654         it('smoke tests nested transformations', function() {
1655             var doc = getDocumentFromXML('<div></div>');
1656
1657             doc.registerExtension({elementNode: {transformations: {
1658                 nested: function(v) {
1659                     this._$.attr('innerAttr', v);
1660                     this.triggerChangeEvent();
1661                 },
1662                 outer: function(v) {
1663                     this.nested(v);
1664                     this._$.attr('outerAttr', v);
1665                     this.triggerChangeEvent();
1666                 }
1667             }}});
1668
1669             doc.root.outer('test1');
1670             doc.root.outer('test2');
1671
1672             expect(doc.root.getAttr('innerAttr')).to.equal('test2');
1673             expect(doc.root.getAttr('outerAttr')).to.equal('test2');
1674
1675             doc.undo();
1676
1677             expect(doc.root.getAttr('innerAttr')).to.equal('test1');
1678             expect(doc.root.getAttr('outerAttr')).to.equal('test1');
1679
1680             doc.undo();
1681
1682             expect(doc.root.getAttr('innerAttr')).to.equal(undefined);
1683             expect(doc.root.getAttr('outerAttr')).to.equal(undefined);
1684
1685             doc.redo();
1686
1687             expect(doc.root.getAttr('innerAttr')).to.equal('test1');
1688             expect(doc.root.getAttr('outerAttr')).to.equal('test1');
1689
1690             doc.redo();
1691
1692             expect(doc.root.getAttr('innerAttr')).to.equal('test2');
1693             expect(doc.root.getAttr('outerAttr')).to.equal('test2');
1694
1695         });
1696
1697         it('ignores transformation if document didn\'t emit change event', function() {
1698             var doc = getDocumentFromXML('<div></div>');
1699
1700             doc.registerExtension({elementNode: {transformations: {
1701                 test: function() {
1702                     // empty
1703                 }
1704             }}});
1705
1706             doc.root.test();
1707             expect(doc.undoStack.length).to.equal(0);
1708
1709         });
1710
1711         describe('Transactions', function() {
1712             it('allows to undo/redo series of transformations at once', function() {
1713                 var doc = getDocumentFromXML('<div></div>');
1714
1715                 doc.registerExtension({
1716                     elementNode: {transformations: {
1717                         test: function(v) {
1718                             this.setAttr('test', v);
1719                         }
1720                     }}
1721                 });
1722
1723                 doc.startTransaction();
1724                 doc.root.test('1');
1725                 doc.root.test('2');
1726                 doc.root.test('3');
1727                 doc.endTransaction();
1728
1729                 doc.undo();
1730                 expect(doc.root.getAttr('test'), '1');
1731                 doc.redo();
1732                 expect(doc.root.getAttr('test'), '3');
1733                 doc.undo();
1734                 expect(doc.root.getAttr('test'), '1');
1735                 doc.redo();
1736                 expect(doc.root.getAttr('test'), '3');
1737             });
1738
1739             it('ignores empty transactions', function() {
1740                 var doc = getDocumentFromXML('<div></div>');
1741                 doc.startTransaction();
1742                 doc.endTransaction();
1743                 expect(doc.undoStack).to.have.length(0, 'empty transaction doesn\'t get pushed into undo stack');
1744             });
1745
1746             it('doesn\'t break on optimizations', function() {
1747                 // This is a smoke test checking if optimizations made to transaction undoing
1748                 // doesnt't break anything.
1749                 var doc = getDocumentFromXML('<div smart="1" unaware="1"></div>');
1750
1751                 doc.registerExtension({
1752                     elementNode: {transformations: {
1753                         unaware: function(v) {
1754                             this.setAttr('unware', v);
1755                             this.triggerChangeEvent();
1756                         },
1757                         smart: {
1758                             impl: function(t, v) {
1759                                 t.oldVal = this.getAttr('smart');
1760                                 this.setAttr('smart', v);
1761                                 this.triggerChangeEvent();
1762                             },
1763                             undo: function(t) {
1764                                 this.setAttr('smart', t.oldVal);
1765                                 this.triggerChangeEvent();
1766                             }
1767                         }
1768                     }}
1769                 });
1770
1771                 doc.startTransaction();
1772                 doc.root.smart('2');
1773                 doc.root.unaware('2');
1774                 doc.root.smart('3');
1775                 doc.root.unaware('3');
1776                 doc.endTransaction();
1777
1778                 doc.undo();
1779
1780                 expect(doc.root.getAttr('smart')).to.equal('1');
1781                 expect(doc.root.getAttr('unaware')).to.equal('1');
1782             });
1783
1784             it('can have associated metadata', function() {
1785                 var doc = getDocumentFromXML('<div></div>'),
1786                     metadata = Object.create({});
1787
1788                 doc.registerExtension({document: {transformations: {
1789                     test: function() {
1790                         this.trigger('change');
1791                     }
1792                 }}});
1793
1794                 doc.startTransaction(metadata);
1795                 doc.test();
1796                 doc.endTransaction();
1797
1798                 var transaction = doc.undoStack[0];
1799                 expect(transaction.metadata).to.equal(metadata);
1800             });
1801
1802             it('can be rolled back', function() {
1803                 var doc = getDocumentFromXML('<root></root>');
1804
1805                 doc.startTransaction();
1806                 doc.root.append({tagName: 'div'});
1807                 doc.rollbackTransaction();
1808
1809                 expect(doc.undoStack.length).to.equal(0, 'nothing to undo');
1810                 expect(doc.root.contents().length).to.equal(0);
1811             });
1812
1813             it('rollbacks and calls error handleor if error gets thrown', function() {
1814                 var doc = getDocumentFromXML('<root></root>'),
1815                     err = new Error(),
1816                     spy = sinon.spy();
1817                 
1818                 doc.transaction(function() {
1819                     doc.root.append({tagName: 'div'});
1820                     throw err;
1821                 }, {error: spy});
1822
1823                 expect(spy.args[0][0]).to.equal(err);
1824                 expect(doc.root.contents().length).to.equal(0);
1825                 expect(doc.undoStack.length).to.equal(0);
1826             });
1827         });
1828
1829         describe('Regression tests', function() {
1830             it('redos correctly after running its own undo followed by unaware transformation undo', function() {
1831                 var doc = getDocumentFromXML('<section t="0"></section>');
1832                 
1833                 doc.registerExtension({elementNode: {transformations: {
1834                     unaware: function() {
1835                         this.triggerChangeEvent();
1836                     },
1837                     test: {
1838                         impl: function() {
1839                             this._$.attr('t', 1);
1840                             this.triggerChangeEvent();
1841                         },
1842                         undo: function() {
1843                             this._$.attr('t', 0);
1844                         }
1845                     }
1846                 }}});
1847                 doc.root.unaware();
1848                 doc.root.test();
1849                 doc.undo();
1850                 doc.undo();
1851                 doc.redo();
1852                 doc.redo();
1853                 expect(doc.root.getAttr('t')).to.equal('1');
1854             });
1855             it('can perform undo of an operation performed after automatic transaction rollback', function() {
1856                 var doc = getDocumentFromXML('<section></section>'),
1857                     extension = {document: {transformations: {
1858                         throwingTransformation: function() { throw new Error(); }
1859                     }}};
1860
1861                 doc.registerExtension(extension);
1862
1863                 doc.throwingTransformation();
1864
1865                 doc.transaction(function() {
1866                     doc.root.setAttr('x', '2');
1867                 });
1868
1869                 expect(doc.undoStack.length).to.equal(1);
1870                 expect(doc.root.getAttr('x')).to.equal('2');
1871
1872                 doc.undo();
1873
1874                 expect(doc.undoStack.length).to.equal(0);
1875                 expect(doc.root.getAttr('x')).to.be.undefined;
1876
1877             });
1878         });
1879     });
1880
1881 });
1882
1883 });