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