smartxml: Cache DocumentNode instances in the underlying DOM structure
[fnpeditor.git] / src / editor / modules / documentCanvas / canvas / canvas.test.js
1 define([
2 'libs/jquery',
3 'libs/chai',
4 'libs/sinon',
5 'modules/documentCanvas/canvas/canvas',
6 'modules/documentCanvas/canvas/utils',
7 'modules/documentCanvas/canvas/documentElement',
8 'wlxml/wlxml',
9 ], function($, chai, sinon, canvas, utils, documentElement, wlxml) {
10     
11 'use strict';
12 /* global describe, it, beforeEach, afterEach */
13
14 var expect = chai.expect;
15
16 var getCanvasFromXML = function(xml, elements) {
17     return canvas.fromXMLDocument(getDocumentFromXML(xml), elements);
18 };
19
20 var getDocumentFromXML = function(xml) {
21     return wlxml.WLXMLDocumentFromXML(xml);
22 };
23
24 var wait = function(callback, timeout) {
25     /* globals window */
26     return window.setTimeout(callback, timeout || 0.5);
27 };
28
29
30 describe('wtf', function() {
31     it('wtf!', function() {
32         var c = getCanvasFromXML('<section>Alice</section>'),
33             doc = c.wlxmlDocument;
34
35         var txtNode = doc.root.contents()[0];
36         txtNode.wrapWith({tagName: 'header', start: 1, end: 2});
37         expect(c.doc().children().length).to.equal(3);
38     });
39 });
40
41 describe('new Canvas', function() {
42     it('abc', function() {
43         var doc = wlxml.WLXMLDocumentFromXML('<section>Alice <span>has</span> a cat!</div>'),
44             c = canvas.fromXMLDocument(doc);
45
46         expect(c.doc().children()).to.have.length(3);
47         expect(c.doc().children()[0].canvas).to.equal(c);
48         expect(c.doc().children()[0].wlxmlNode.sameNode(doc.root));
49     });
50 });
51
52 describe('Handling empty text nodes', function() {
53     it('puts zero width space into node with about to be remove text', function(done) {
54         var c = getCanvasFromXML('<section>Alice</section>'),
55             textElement = c.doc().children()[0];
56         textElement.setText('');
57
58         /* Wait for MutationObserver to kick in. */
59         wait(function() {
60             expect(textElement.getText({raw:true})).to.equal(utils.unicode.ZWS, 'ZWS in canvas');
61             expect(c.wlxmlDocument.root.contents()[0].getText()).to.equal('', 'empty string in a document');
62             done();
63         });
64     });
65 });
66
67 describe('Handling changes to the document', function() {
68     it('replaces the whole canvas content when document root node replaced', function() {
69         var doc = getDocumentFromXML('<section></section>'),
70             c = canvas.fromXMLDocument(doc);
71
72         var header = doc.root.replaceWith({tagName: 'header'});
73         expect(c.doc().wlxmlNode.sameNode(header)).to.equal(true);
74     });
75 });
76
77 describe('Listening to document changes', function() {
78
79     it('Handling element node moved', function() {
80         var doc = getDocumentFromXML('<section><a></a><b></b></section>'),
81             a = doc.root.contents()[0],
82             b = doc.root.contents()[1],
83             c = canvas.fromXMLDocument(doc);
84
85         a.before(b);
86         var sectionChildren = c.doc().children();
87         expect(sectionChildren.length).to.equal(2);
88         expect(sectionChildren[0].wlxmlNode.getTagName()).to.equal('b');
89         expect(sectionChildren[1].wlxmlNode.getTagName()).to.equal('a');
90     });
91
92     it('Handling text node moved', function() {
93         var doc = getDocumentFromXML('<section><a></a>Alice</section>'),
94             a = doc.root.contents()[0],
95             textNode = doc.root.contents()[1],
96             c = canvas.fromXMLDocument(doc);
97
98         a.before(textNode);
99         var sectionChildren = c.doc().children();
100         expect(sectionChildren.length).to.equal(2);
101         expect(sectionChildren[0].getText()).to.equal('Alice');
102         expect(sectionChildren[1].wlxmlNode.getTagName()).to.equal('a');
103     });
104
105     it('Handles nodeTagChange event', function() {
106
107         var doc = wlxml.WLXMLDocumentFromXML('<section><div>Alice</div></section>'),
108             c = canvas.fromXMLDocument(doc);
109
110         doc.root.contents()[0].setTag('header');
111
112         var headerNode = doc.root.contents()[0],
113             headerElement = c.doc().children()[0];
114
115         expect(headerElement.wlxmlNode.getTagName()).to.equal('header', 'element ok');
116
117         /* Make sure we handle invalidation of reference to wlxmlNode after changing its tag */
118         expect(headerNode.getData('canvasElement').sameNode(headerElement)).to.equal(true, 'node->element');
119         expect(headerElement.wlxmlNode.sameNode(headerNode)).to.equal(true, 'element->node');
120     });
121
122     it('Handles nodeDetached event for an empty text node', function(done) {
123         var doc = wlxml.WLXMLDocumentFromXML('<section><div>A<span>b</span></div></section>'),
124             aTextNode = doc.root.contents()[0].contents()[0],
125             aTextElement;
126
127         canvas.fromXMLDocument(doc);
128         aTextElement = utils.getElementForNode(aTextNode);
129
130         aTextElement.setText('');
131
132         wait(function() {
133             var parent = aTextElement.parent();
134             expect(aTextElement.getText({raw:true})).to.equal(utils.unicode.ZWS, 'canvas represents this as empty node');
135             aTextElement.wlxmlNode.detach();
136             expect(parent.children().length).to.equal(1);
137             expect(parent.children()[0].wlxmlNode.getTagName()).to.equal('span');
138             done();
139         });
140     });
141 });
142
143 describe('Displaying span nodes', function() {
144     it('inlines a span element with a text', function() {
145         var c = getCanvasFromXML('<section><span>Alice</span></section>'),
146             spanElement = c.doc().children()[0];
147         expect(spanElement.isBlock()).to.equal(false);
148     });
149     it('renders non-span element as a block', function() {
150         var c = getCanvasFromXML('<section><span></span></section>'),
151             element = c.doc().children()[0],
152             node = element.wlxmlNode;
153
154         expect(element.isBlock()).to.equal(false, 'initially inline');
155         node = node.setTag('div');
156         expect(node.getData('canvasElement').isBlock()).to.equal(true, 'block');
157     });
158
159     it('inlines a span element if its block content gets removed', function() {
160         var c = getCanvasFromXML('<section><span>Alice <div>has</div> a cat!</span></section>'),
161             spanElement = c.doc().children()[0],
162             divNode = spanElement.wlxmlNode.contents()[1];
163
164         expect(spanElement.isBlock()).to.equal(true, 'initially a block');
165         divNode.detach();
166         expect(spanElement.isBlock()).to.equal(false, 'inlined after removing inner block');
167         
168         spanElement.wlxmlNode.append({tagName: 'div'});
169
170         expect(spanElement.isBlock()).to.equal(true, 'block again after bringing back inner block');
171     });
172
173     it('keeps showing element as a block after changing its node tag to span if it contains elements of non-span nodes', function() {
174         var c = getCanvasFromXML('<section><div><div></div></div></section>'),
175             outerDivElement = c.doc().children()[0],
176             outerDivNode = outerDivElement.wlxmlNode;
177         outerDivNode = outerDivNode.setTag('span');
178         expect(c.doc().children()[0].isBlock()).to.equal(true);
179     });
180 });
181
182
183 describe('Default document changes handling', function() {
184     it('handles added node', function() {
185         var c = getCanvasFromXML('<section></section>');
186         c.wlxmlDocument.root.append({tagName:'div'});
187         expect(c.doc().children().length).to.equal(1);
188         c.wlxmlDocument.root.prepend({tagName:'div'});
189         expect(c.doc().children().length).to.equal(2);
190
191         var node = c.wlxmlDocument.root.contents()[1];
192         node.before({tagName: 'div'});
193         expect(c.doc().children().length).to.equal(3);
194         node.after({tagName: 'div'});
195         expect(c.doc().children().length).to.equal(4);
196     });
197
198     it('handles attribute value change for a class attribute', function() {
199         var c = getCanvasFromXML('<section></section>');
200         c.wlxmlDocument.root.setAttr('class', 'test');
201         expect(c.doc().wlxmlNode.getClass()).to.equal('test');
202     });
203
204     it('handles detached node', function() {
205         var c = getCanvasFromXML('<section><div></div></section>');
206         c.wlxmlDocument.root.contents()[0].detach();
207         expect(c.doc().children().length).to.equal(0);
208     });
209
210     it('handles moved node', function() {
211         var doc = getDocumentFromXML('<section><c></c><a></a><b></b></section>'),
212             c = doc.root.contents()[0],
213             a = doc.root.contents()[1],
214             b = doc.root.contents()[2],
215             cv = canvas.fromXMLDocument(doc);
216
217         a.document.transaction(function() {
218             a.before(b); // => cab
219             b.after(c); // => bca
220         }, {
221             error: function(e) {throw e;}
222         });
223
224         var sectionChildren = cv.doc().children();
225         expect(sectionChildren.length).to.equal(3);
226         expect(sectionChildren[0].wlxmlNode.getTagName()).to.equal('b');
227         expect(sectionChildren[1].wlxmlNode.getTagName()).to.equal('c');
228         expect(sectionChildren[2].wlxmlNode.getTagName()).to.equal('a');
229     });
230
231     it('handles moving text node to another parent', function() {
232         var c = getCanvasFromXML('<section>Alice<div><span>has</span></div>a cat.</section>'),
233             doc = c.wlxmlDocument,
234             text = doc.root.contents()[0],
235             div = doc.root.contents()[1];
236         
237         div.append(text);
238         
239         var sectionChildren = c.doc().children();
240         expect(sectionChildren.length).to.equal(2);
241         expect(sectionChildren[0].wlxmlNode.sameNode(div)).to.equal(true);
242         expect(sectionChildren[1].getText()).to.equal('a cat.');
243
244         expect(div.contents().length).to.equal(2);
245         expect(div.contents()[0].getTagName()).to.equal('span');
246         expect(div.contents()[1].getText()).to.equal('Alice');
247     });
248
249     it('handles change in a text node', function() {
250         var c = getCanvasFromXML('<section>Alice</section>');
251         c.wlxmlDocument.root.contents()[0].setText('cat');
252         expect(c.doc().children()[0].getText()).to.equal('cat');
253     });
254
255     describe('Regression tests', function() {
256         it('handles moving node after its next neighbour correctly', function() {
257             var c = getCanvasFromXML('<section><a></a><b></b></section>'),
258                 doc = c.wlxmlDocument,
259                 a = doc.root.contents()[0],
260                 b = doc.root.contents()[1];
261             b.after(a);
262             var sectionChildren = c.doc().children();
263             expect(sectionChildren[0].wlxmlNode.getTagName()).to.equal('b');
264             expect(sectionChildren[1].wlxmlNode.getTagName()).to.equal('a');
265         });
266     });
267 });
268     
269 describe('Custom elements based on wlxml class attribute', function() {
270     it('allows custom rendering', function() {
271         var prototype = $.extend({}, documentElement.DocumentNodeElement.prototype, {
272                 init: function() {
273                     this._container().append('<test></test>');
274                 }
275             }),
276             c = getCanvasFromXML('<section><div class="testClass"></div></section>', [
277             {tag: 'div', klass: 'testClass', prototype: prototype}
278         ]);
279
280         expect(c.doc().children()[0]._container().children('test').length).to.equal(1); // @!
281     });
282
283     it('allows handling changes to internal structure of rendered node', function() {
284         var prototype = $.extend({}, documentElement.DocumentNodeElement.prototype, {
285                 init: function() {
286                     this.header = $('<h1>');
287                     this._container().append(this.header);
288                     this.refresh2();
289                 },
290                 refresh2: function() {
291                     this.header.text(this.wlxmlNode.contents().length);
292                 },
293                 onNodeAdded: function(event) {
294                     void(event);
295                     this.refresh2();
296                 },
297                 onNodeTextChange: function(event) {
298                     this.header.text(event.meta.node.getText());
299                 },
300                 children: function() { return []; }
301         });
302
303         var c = getCanvasFromXML('<section><div class="testClass"><a></a></div></section>', [
304             {tag: 'div', klass: 'testClass', prototype: prototype}
305         ]);
306
307         var node = c.wlxmlDocument.root.contents()[0],
308             element = node.getData('canvasElement');
309
310         var header = element.dom.find('h1');
311         expect(header.text()).to.equal('1', 'just <a>');
312
313         node.append({tagName: 'div'});
314
315         expect(header.text()).to.equal('2', 'added div');
316
317         var textNode = node.append({text: 'test'});
318
319         expect(header.text()).to.equal('3', 'added text node');
320         
321         textNode.setText('test2');
322
323         expect(header.text()).to.equal('test2', 'text node change handled');
324     });
325
326     describe('Handling unknown class', function() {
327         it('Inherits default behavior', function() {
328             var c = getCanvasFromXML('<section><div class="unknown">Hi!</div></section>');
329             expect(c.doc().children()[0].children()[0].getText()).to.equal('Hi!');
330         });
331     });
332 });
333
334 describe('Cursor', function() {
335     /* globals Node */
336     var getSelection;
337
338     var findTextNode = function(inside, text) {
339         var nodes = inside.find(':not(iframe)').addBack().contents().filter(function() {
340             return this.nodeType === Node.TEXT_NODE && this.data === text;
341         });
342         if(nodes.length) {
343             return nodes[0];
344         }
345         return null;
346     };
347
348     beforeEach(function() {
349         /* globals window */
350         getSelection = sinon.stub(window, 'getSelection');
351     });
352
353     afterEach(function() {
354         getSelection.restore();
355     });
356
357     it('returns position when browser selection collapsed', function() {
358         var c = getCanvasFromXML('<section>Alice has a cat</section>'),
359             dom = c.doc().dom,
360             text = findTextNode(dom, 'Alice has a cat');
361
362         expect(text.nodeType).to.equal(Node.TEXT_NODE, 'correct node selected');
363         expect($(text).text()).to.equal('Alice has a cat');
364
365         getSelection.returns({
366             anchorNode: text,
367             focusNode: text,
368             anchorOffset: 5,
369             focusOffset: 5,
370             isCollapsed: true
371         });
372         var cursor = c.getCursor(),
373             position = cursor.getPosition();
374
375         expect(cursor.isSelecting()).to.equal(false, 'cursor is not selecting anything');
376         expect(position.element.getText()).to.equal('Alice has a cat');
377         expect(position.offset).to.equal(5);
378         expect(position.offsetAtEnd).to.equal(false, 'offset is not at end');
379
380         getSelection.returns({
381             anchorNode: text,
382             focusNode: text,
383             anchorOffset: 15,
384             focusOffset: 15,
385             isCollapsed: true
386         });
387
388         expect(cursor.getPosition().offsetAtEnd).to.equal(true, 'offset at end');
389     });
390
391     it('recognizes selection start and end on document order', function() {
392         var c = getCanvasFromXML('<section><span>Alice</span><span>has a cat</span><div>abc<span>...</span>cde</div></section>'),
393             dom = c.doc().dom,
394             textFirst = findTextNode(dom, 'Alice'),
395             textSecond = findTextNode(dom, 'has a cat'),
396             textAbc = findTextNode(dom, 'abc'),
397             textCde = findTextNode(dom, 'cde');
398
399         var check = function(label, expected) {
400             var cursor = c.getCursor();
401             label = label + ': ';
402             expect(cursor.getSelectionStart().element.getText()).to.equal(expected.start.text, label + 'start element ok');
403             expect(cursor.getSelectionStart().offset).to.equal(expected.start.offset, label + 'start offset ok');
404             expect(cursor.getSelectionEnd().element.getText()).to.equal(expected.end.text, label + 'end element ok');
405             expect(cursor.getSelectionEnd().offset).to.equal(expected.end.offset, label + 'end offset ok');
406         };
407
408         getSelection.returns({
409             anchorNode: textFirst,
410             focusNode: textFirst,
411             anchorOffset: 1,
412             focusOffset: 3,
413             isCollapsed: false
414         });
415
416         check('same element, anchor first', {
417             start: {text: 'Alice', offset: 1},
418             end: {text: 'Alice', offset:3}
419         });
420
421
422         getSelection.returns({
423             anchorNode: textFirst,
424             focusNode: textFirst,
425             anchorOffset: 3,
426             focusOffset: 1,
427             isCollapsed: false
428         });
429
430         check('same element, anchor second', {
431             start: {text: 'Alice', offset: 1},
432             end: {text: 'Alice', offset:3}
433         });
434
435
436         getSelection.returns({
437             anchorNode: textAbc,
438             focusNode: textCde,
439             anchorOffset: 3,
440             focusOffset: 1,
441             isCollapsed: false
442         });
443
444         check('same parent, anchor first', {
445             start: {text: 'abc', offset: 3},
446             end: {text: 'cde', offset:1}
447         });
448
449
450         getSelection.returns({
451             anchorNode: textCde,
452             focusNode: textAbc,
453             anchorOffset: 1,
454             focusOffset: 3,
455             isCollapsed: false
456         });
457
458         check('same parent, anchor second', {
459             start: {text: 'abc', offset: 3},
460             end: {text: 'cde', offset:1}
461         });
462
463
464         getSelection.returns({
465             anchorNode: textFirst,
466             focusNode: textSecond,
467             anchorOffset: 1,
468             focusOffset: 3,
469             isCollapsed: false
470         });
471
472         check('different parents, anchor first', {
473             start: {text: 'Alice', offset: 1},
474             end: {text: 'has a cat', offset:3}
475         });
476
477
478         getSelection.returns({
479             anchorNode: textSecond,
480             focusNode: textFirst,
481             anchorOffset: 3,
482             focusOffset: 1,
483             isCollapsed: false
484         });
485
486         check('different parents, anchor second', {
487             start: {text: 'Alice', offset: 1},
488             end: {text: 'has a cat', offset:3}
489         });
490     });
491
492     it('returns boundries of selection when browser selection not collapsed', function() {
493         var c = getCanvasFromXML('<section>Alice <span>has</span> a <span>big</span> cat</section>'),
494             dom = c.doc().dom,
495             text = {
496                 alice: findTextNode(dom, 'Alice '),
497                 has: findTextNode(dom, 'has'),
498                 cat: findTextNode(dom, ' cat')
499             },
500             cursor = c.getCursor(),
501             aliceElement = c.getDocumentElement(text.alice),
502             catElement = c.getDocumentElement(text.cat);
503
504         [
505             {focus: text.alice, focusOffset: 1, anchor: text.cat,   anchorOffset: 2, selectionAnchor: catElement},
506             {focus: text.cat,   focusOffset: 2, anchor: text.alice, anchorOffset: 1, selectionAnchor: aliceElement}
507         ].forEach(function(s, idx) {
508             getSelection.returns({isColapsed: false, anchorNode: s.anchor, anchorOffset: s.anchorOffset, focusNode: s.focus, focusOffset: s.focusOffset});
509
510             var selectionStart = cursor.getSelectionStart(),
511                 selectionEnd = cursor.getSelectionEnd(),
512                 selectionAnchor = cursor.getSelectionAnchor();
513
514             expect(cursor.isSelecting()).to.equal(true, 'cursor is selecting');
515             expect(selectionStart.element.sameNode(aliceElement)).to.equal(true, '"Alice" is the start of the selection ' + idx);
516             expect(selectionStart.offset).to.equal(1, '"Alice" offset ok' + idx);
517             expect(selectionEnd.element.sameNode(catElement)).to.equal(true, '"Cat" is the start of the selection ' + idx);
518             expect(selectionEnd.offset).to.equal(2, '"Cat" offset ok' + idx);
519             expect(selectionAnchor.element.sameNode(s.selectionAnchor)).to.equal(true, 'anchor ok');
520             expect(selectionAnchor.offset).to.equal(s.anchorOffset, 'anchor offset ok');
521         });
522     });
523
524     it('recognizes when browser selection boundries lies in sibling DocumentTextElements', function() {
525         var c = getCanvasFromXML('<section>Alice <span>has</span> a <span>big</span> cat</section>'),
526             dom = c.doc().dom,
527             text = {
528                 alice: findTextNode(dom, 'Alice '),
529                 has: findTextNode(dom, 'has'),
530                 a: findTextNode(dom, ' a '),
531                 big: findTextNode(dom, 'big'),
532                 cat: findTextNode(dom, ' cat'),
533             },
534             cursor = c.getCursor();
535
536         expect($(text.alice).text()).to.equal('Alice ');
537         expect($(text.has).text()).to.equal('has');
538         expect($(text.a).text()).to.equal(' a ');
539         expect($(text.big).text()).to.equal('big');
540         expect($(text.cat).text()).to.equal(' cat');
541
542         getSelection.returns({anchorNode: text.alice, focusNode: text.a});
543         expect(cursor.isSelectingSiblings()).to.equal(true, '"Alice" and "a" are children');
544
545         getSelection.returns({anchorNode: text.alice, focusNode: text.cat});
546         expect(cursor.isSelectingSiblings()).to.equal(true, '"Alice" and "cat" are children');
547
548         getSelection.returns({anchorNode: text.alice, focusNode: text.has});
549         expect(cursor.isSelectingSiblings()).to.equal(false, '"Alice" and "has" are not children');
550
551         getSelection.returns({anchorNode: text.has, focusNode: text.big});
552         expect(cursor.isSelectingSiblings()).to.equal(false, '"has" and "big" are not children');
553     });
554 });
555
556 });