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