splitting blocks from spans
[fnpeditor.git] / src / editor / plugins / core / core.test.js
1 define(function(require) {
2     
3 'use strict';
4
5 /* globals describe, it, afterEach */
6
7 var $ = require('libs/jquery'),
8     _ = require('libs/underscore'),
9     chai = require('libs/chai'),
10     sinon = require('libs/sinon'),
11     wlxml = require('wlxml/wlxml'),
12     canvas = require('modules/documentCanvas/canvas/canvas'),
13     keyboard = require('modules/documentCanvas/canvas/keyboard'),
14     keyEvent = require('modules/documentCanvas/canvas/keyEvent'),
15     corePlugin = require('./core.js'),
16     expect = chai.expect;
17
18 var K = keyboard.KEYS;
19
20 var getDocumentFromXML = function(xml, options) {
21     var doc = wlxml.WLXMLDocumentFromXML(xml, options || {});
22     doc.registerExtension(corePlugin.documentExtension);
23     return doc;
24 };
25
26
27 var getCanvasFromXML = function(xml, elements) {
28     var c = canvas.fromXMLDocument(getDocumentFromXML(xml), elements),
29         view = c.view();
30     view.attr('canvas-test', true);
31     /* globals document */
32     $(document.body).append(view);
33     return c;
34 };
35 var removeCanvas = function() {
36     $('[canvas-test]').remove();
37 };
38
39 var getTextNodes = function(text, doc) {
40     /* globals Node */
41     var toret = [];
42     var search = function(node) {
43         node.contents().forEach(function(node) {
44             if(node.nodeType === Node.TEXT_NODE) {
45                 if(node.getText() === text) {
46                     toret.push(node);
47                 }
48             } else {
49                 search(node);
50             }
51         });
52     };
53     search(doc.root);
54     return toret;
55 };
56
57 var getTextNode = function(text, doc) {
58     var nodes = getTextNodes(text, doc),
59         error;
60     if(nodes.length === 0) {
61         error = 'Text not found';
62     } else if(nodes.length > 1) {
63         error = 'Text not unique';
64     } else if(nodes[0].getText() !== text) {
65         error = 'I was trying to cheat your test :(';
66     }
67     if(error) {
68         throw new Error(error);
69     }
70     return nodes[0];
71 };
72
73 var getTextElement = function(text, c) {
74     var node = getTextNode(text, c.wlxmlDocument),
75         element =  node && node.getData('canvasElement');
76     if(!(element && element.getText() === text)) {
77         throw new Error();
78     }
79     return element;
80 };
81
82
83 describe('Document extensions', function() {
84     describe('break content', function() {
85         it('break text into two nodes', function() {
86             var doc = getDocumentFromXML('<section><div>Alice</div></section>'),
87                 textNode = doc.root.contents()[0].contents()[0];
88             
89             var result = textNode.breakContent({offset:3});
90
91             var section = doc.root;
92             expect(section.contents().length).to.equal(2);
93             expect(section.contents()[0].contents()[0].getText()).to.equal('Ali');
94             expect(section.contents()[1].contents()[0].getText()).to.equal('ce');
95
96             expect(result.first.sameNode(section.contents()[0])).to.equal(true);
97             expect(result.second.sameNode(section.contents()[1])).to.equal(true);
98             expect(result.emptyText).to.equal(undefined, 'no new text node created');
99         });
100         it('puts empty text node when breaking at the very beginning', function() {
101             var doc = getDocumentFromXML('<section><div>Alice</div></section>'),
102                 textNode = doc.root.contents()[0].contents()[0];
103             
104             var result = textNode.breakContent({offset:0}),
105                 firstNode = doc.root.contents()[0];
106
107             expect(result.emptyText.sameNode(firstNode.contents()[0]));
108             expect(result.emptyText.getText()).to.equal('');
109         });
110         it('puts empty text node when breaking at the very end', function() {
111             var doc = getDocumentFromXML('<section><div>Alice</div></section>'),
112             textNode = doc.root.contents()[0].contents()[0];
113             
114             var result = textNode.breakContent({offset:5}),
115                 secondNode = doc.root.contents()[1];
116             
117             expect(result.emptyText.sameNode(secondNode.contents()[0]));
118             expect(result.emptyText.getText()).to.equal('');
119         });
120     });
121
122     describe('mergin text with preceding content', function() {
123         it('does nothing if text node parent has no preceding element', function() {
124             var doc = getDocumentFromXML('<section><div><div>some text</div></div></section>'),
125                 text = getTextNode('some text', doc),
126                 spy = sinon.spy();
127
128             doc.on('change', spy);
129             text.mergeContentUp();
130             expect(spy.callCount).to.equal(0);
131         });
132         it('does nothing if text node parent is precedeed by text node', function() {
133             var doc = getDocumentFromXML('<section><div></div>another text<div>some text</div></section>'),
134                 text = getTextNode('some text', doc),
135                 spy = sinon.spy();
136
137             doc.on('change', spy);
138             text.mergeContentUp();
139             expect(spy.callCount).to.equal(0);
140         });
141         it('does nothing if text node is not first child of its parent', function() {
142             var doc = getDocumentFromXML('<section><div></div><div><a></a>some text</div></section>'),
143                 text = getTextNode('some text', doc),
144                 spy = sinon.spy();
145
146             doc.on('change', spy);
147             text.mergeContentUp();
148             expect(spy.callCount).to.equal(0);
149         });
150         it('moves text node and its siblings to the block element preceding text node parent', function() {
151             var doc = getDocumentFromXML('<section><div></div><div>some text<span>is</span> here!</div></section>'),
152                 text = getTextNode('some text', doc);
153             
154             text.mergeContentUp();
155             
156             var contents = doc.root.contents();
157             expect(contents.length).to.equal(1);
158             expect(contents[0].contents().length).to.equal(3);
159             expect(contents[0].contents()[0].getText()).to.equal('some text');
160             expect(contents[0].contents()[1].getTagName()).to.equal('span');
161             expect(contents[0].contents()[2].getText()).to.equal(' here!');
162         });
163     });
164 });
165
166 describe('Keyboard interactions', function() {
167
168     var Keyboard = function(canvas) {
169         this.canvas = canvas;
170     };
171
172     _.extend(Keyboard.prototype, {
173         press: function(key) {
174             this.canvas.triggerKeyEvent(keyEvent.fromParams({key:key}), this.selection);
175             this.selection = this.canvas.getSelection();
176             return this;
177         },
178         withCaret: function(where) {
179             var offset = where.indexOf('|'),
180                 text = where.split('|').join(''),
181                 el = getTextElement(text, this.canvas),
182                 selection = this.canvas.createSelection({type: 'caret', element: el, offset: offset});
183             if(offset === -1) {
184                 throw new Error('Invalid caret');
185             }
186             this.selection = selection;
187             return this;
188         },
189         withSelection: function(start, end) {
190             var startOffset = start.indexOf('|'),
191                 endOffset = end.indexOf('|'),
192                 startText= start.split('|').join(''),
193                 endText = end.split('|').join(''),
194                 startElement = getTextElement(startText, this.canvas),
195                 endElement = getTextElement(endText, this.canvas),
196                 selection = this.canvas.createSelection({
197                     type: 'textSelection', 
198                     anchorElement: startElement,
199                     anchorOffset: startOffset,
200                     focusElement: endElement,
201                     focusOffset: endOffset
202                 });
203             if(startOffset === -1 || endOffset === -1) {
204                 throw new Error('Invalid text selection');
205             }
206             this.selection = selection;
207             return this;    
208         }
209     });
210
211     describe('deleting text with selection', function() {
212         afterEach(removeCanvas);
213
214         [K.BACKSPACE, K.DELETE].forEach(function(key) {
215             it('deletes text withing a single text element ' + key, function() {
216                 var c = getCanvasFromXML('<section><div>Alice</div></section>'),
217                     k = new Keyboard(c);
218
219                 k.withSelection('A|lice', 'Alic|e').press(key);
220                 expect(c.wlxmlDocument.root.contents()[0].contents()[0].getText()).to.equal('Ae');
221
222
223                 var selection = c.getSelection();
224                 expect(selection.type).to.equal('caret');
225                 expect(selection.element.getText()).to.equal('Ae');
226                 expect(selection.offset).to.equal(1);
227             });
228             it('deletes text across two paragraphs ' + key, function() {
229                 var c = getCanvasFromXML('<section><div class="p">Alice</div><div class="p">cat</div></section>'),
230                     k = new Keyboard(c);
231
232                 k.withSelection('A|lice', 'c|at').press(key);
233                 var rootContents = c.wlxmlDocument.root.contents();
234
235                 expect(rootContents.length).to.equal(2);
236                 expect(rootContents[0].contents()[0].getText()).to.equal('A');
237                 expect(rootContents[1].contents()[0].getText()).to.equal('at');
238
239                 var selection = c.getSelection();
240                 expect(selection.type).to.equal('caret');
241                 expect(selection.element.wlxmlNode.getText()).to.equal(key === K.BACKSPACE ? 'A' : 'at');
242             });
243
244             it('keeps an empty paragraph after deleting its whole text ' + key, function() {
245                 var c = getCanvasFromXML('<section><div class="p">Alice</div></section>'),
246                     k = new Keyboard(c);
247
248                 k.withSelection('|Alice', 'Alice|').press(key);
249                 var rootContents = c.wlxmlDocument.root.contents();
250
251                 expect(rootContents.length).to.equal(1);
252                 expect(rootContents[0].contents()[0].getText()).to.equal('');
253                 
254                 var selection = c.getSelection();
255                 expect(selection.type).to.equal('caret');
256                 expect(selection.element.wlxmlNode.parent().sameNode(c.wlxmlDocument.root.contents()[0]));
257             });
258         });
259
260     });
261
262
263     describe('backspace at the beginning', function() {
264         afterEach(removeCanvas);
265
266         it('merges two adjacent paragraphs', function() {
267             var c = getCanvasFromXML('<section><div class="p">A</div><div class="p">B</div></section>'),
268                 k = new Keyboard(c);
269
270             k.withCaret('|B').press(K.BACKSPACE);
271
272             var rootContents = c.wlxmlDocument.root.contents();
273             expect(rootContents.length).to.equal(1);
274             expect(rootContents[0].getClass()).to.equal('p');
275             expect(rootContents[0].contents()[0].getText()).to.equal('AB');
276
277             var selection = c.getSelection();
278             expect(selection.type).to.equal('caret');
279             expect(selection.element.sameNode(getTextElement('AB', c))).to.equal(true);
280             expect(selection.offset).to.equal(1);
281         });
282         it('merges a paragraph with a header', function() {
283             var c = getCanvasFromXML('<section><header>A</header><div class="p">B</div></section>'),
284                 k = new Keyboard(c);
285
286             k.withCaret('|B').press(K.BACKSPACE);
287
288             var rootContents = c.wlxmlDocument.root.contents();
289             expect(rootContents.length).to.equal(1);
290             expect(rootContents[0].getTagName()).to.equal('header');
291             expect(rootContents[0].contents()[0].getText()).to.equal('AB');
292
293             var selection = c.getSelection();
294             expect(selection.type).to.equal('caret');
295             expect(selection.element.sameNode(getTextElement('AB', c))).to.equal(true);
296             expect(selection.offset).to.equal(1);
297         });
298         it('merges two adjacent headers', function() {
299             var c = getCanvasFromXML('<section><header>A</header><header>B</header></section>'),
300                 k = new Keyboard(c);
301
302             k.withCaret('|B').press(K.BACKSPACE);
303             var rootContents = c.wlxmlDocument.root.contents();
304             expect(rootContents.length).to.equal(1);
305             expect(rootContents[0].getTagName()).to.equal('header');
306             expect(rootContents[0].contents()[0].getText()).to.equal('AB');
307
308             var selection = c.getSelection();
309             expect(selection.type).to.equal('caret');
310             expect(selection.element.sameNode(getTextElement('AB', c))).to.equal(true);
311             expect(selection.offset).to.equal(1);
312         });
313         it('merges a header with a paragraph', function() {
314             var c = getCanvasFromXML('<section><div class="p">A</div><header>B</header></section>'),
315                 k = new Keyboard(c);
316
317             k.withCaret('|B').press(K.BACKSPACE);
318
319             var rootContents = c.wlxmlDocument.root.contents();
320             expect(rootContents.length).to.equal(1);
321             expect(rootContents[0].is('p')).to.equal(true);
322             expect(rootContents[0].contents()[0].getText()).to.equal('AB');
323
324             var selection = c.getSelection();
325             expect(selection.type).to.equal('caret');
326             expect(selection.element.sameNode(getTextElement('AB', c))).to.equal(true);
327             expect(selection.offset).to.equal(1);
328         });
329         it('merges a paragraph into a last list item', function() {
330             var c = getCanvasFromXML('<section><div class="list"><div class="item">item</div></div><div class="p">paragraph</div></section>'),
331                 list = c.wlxmlDocument.root.contents()[0],
332                 k = new Keyboard(c);
333
334             k.withCaret('|paragraph').press(K.BACKSPACE);
335
336             var rootContents = c.wlxmlDocument.root.contents();
337             expect(rootContents.length).to.equal(1);
338             expect(rootContents[0].sameNode(list)).to.equal(true);
339
340             var items = list.contents();
341             expect(items.length).to.equal(1);
342             expect(items[0].contents()[0].getText()).to.equal('itemparagraph');
343
344             var selection = c.getSelection();
345             expect(selection.type).to.equal('caret');
346             expect(selection.element.sameNode(getTextElement('itemparagraph', c))).to.equal(true);
347             expect(selection.offset).to.equal(4);
348         });
349         it('merges a list item with a list item', function() {
350             var c = getCanvasFromXML('<section><div class="list"><div class="item">item1</div><div class="item">item2</div></div></section>'),
351                 list = c.wlxmlDocument.root.contents()[0],
352                 k = new Keyboard(c);
353
354             k.withCaret('|item2').press(K.BACKSPACE);
355
356             var rootContents = c.wlxmlDocument.root.contents();
357             expect(rootContents.length).to.equal(1);
358             
359             expect(rootContents[0].sameNode(list)).to.equal(true);
360
361             var items = list.contents();
362
363             expect(items.length).to.equal(1);
364             expect(items[0].contents()[0].getText()).to.equal('item1item2');
365
366             var selection = c.getSelection();
367             expect(selection.type).to.equal('caret');
368             expect(selection.element.sameNode(getTextElement('item1item2', c))).to.equal(true);
369             expect(selection.offset).to.equal(5);
370         });
371         it('creates a new paragraph preceding the list from a first list item', function() {
372             var c = getCanvasFromXML('<section><div class="list"><div class="item">item1</div><div class="item">item2</div></div></section>'),
373                 list = c.wlxmlDocument.root.contents()[0],
374                 k = new Keyboard(c);
375
376             k.withCaret('|item1').press(K.BACKSPACE);
377
378             var rootContents = c.wlxmlDocument.root.contents();
379             expect(rootContents.length).to.equal(2);
380             
381             expect(rootContents[0].getClass()).to.equal('p');
382             expect(rootContents[0].contents()[0].getText()).to.equal('item1');
383
384             expect(rootContents[1].sameNode(list)).to.equal(true);
385
386             var selection = c.getSelection();
387             expect(selection.type).to.equal('caret');
388             expect(selection.element.sameNode(getTextElement('item1', c))).to.equal(true);
389             expect(selection.offset).to.equal(0);
390         });
391         it('removes list after moving up its only item', function() {
392             var c = getCanvasFromXML('<section><div class="list"><div class="item">item</div></div></section>'),
393                 k = new Keyboard(c);
394
395             k.withCaret('|item').press(K.BACKSPACE);
396             var rootContents = c.wlxmlDocument.root.contents();
397             expect(rootContents.length).to.equal(1);
398             
399             expect(rootContents[0].getClass()).to.equal('p');
400             expect(rootContents[0].contents()[0].getText()).to.equal('item');
401
402             var selection = c.getSelection();
403             expect(selection.type).to.equal('caret');
404             expect(selection.element.sameNode(getTextElement('item', c))).to.equal(true);
405             expect(selection.offset).to.equal(0);
406         });
407     });
408
409     describe('splitting with enter', function() {
410         afterEach(removeCanvas);
411
412         it('splits paragraph into two in the middle', function() {
413             var c = getCanvasFromXML('<section><div class="p">paragraph</div></section>'),
414                 k = new Keyboard(c);
415
416             k.withCaret('para|graph').press(K.ENTER);
417
418             var rootContents = c.wlxmlDocument.root.contents();
419             expect(rootContents.length).to.equal(2);
420             expect(rootContents[0].is({tagName: 'div', klass: 'p'})).to.equal(true);
421             expect(rootContents[0].contents()[0].getText()).to.equal('para');
422             expect(rootContents[1].is({tagName: 'div', klass: 'p'})).to.equal(true);
423             expect(rootContents[1].contents()[0].getText()).to.equal('graph');
424
425             var selection = c.getSelection();
426             expect(selection.type).to.equal('caret');
427             expect(selection.element.sameNode(getTextElement('graph', c))).to.equal(true);
428             expect(selection.offset).to.equal(0);
429         });
430         it('splits paragraph into two at the beginning', function() {
431             var c = getCanvasFromXML('<section><div class="p">paragraph</div></section>'),
432                 k = new Keyboard(c);
433
434             k.withCaret('|paragraph').press(K.ENTER);
435
436             var rootContents = c.wlxmlDocument.root.contents();
437             expect(rootContents.length).to.equal(2);
438             expect(rootContents[0].is({tagName: 'div', klass: 'p'})).to.equal(true);
439             expect(rootContents[0].contents()[0].getText()).to.equal('');
440             expect(rootContents[1].is({tagName: 'div', klass: 'p'})).to.equal(true);
441             expect(rootContents[1].contents()[0].getText()).to.equal('paragraph');
442
443             var selection = c.getSelection();
444             expect(selection.type).to.equal('caret');
445             expect(selection.element.sameNode(getTextElement('', c))).to.equal(true);
446             expect(selection.offset).to.equal(0);
447         });
448         it('splits paragraph into two at the end', function() {
449             var c = getCanvasFromXML('<section><div class="p">paragraph</div></section>'),
450                 k = new Keyboard(c);
451
452             k.withCaret('paragraph|').press(K.ENTER);
453
454             var rootContents = c.wlxmlDocument.root.contents();
455             expect(rootContents.length).to.equal(2);
456             expect(rootContents[0].is({tagName: 'div', klass: 'p'})).to.equal(true);
457             expect(rootContents[0].contents()[0].getText()).to.equal('paragraph');
458             expect(rootContents[1].is({tagName: 'div', klass: 'p'})).to.equal(true);
459             expect(rootContents[1].contents()[0].getText()).to.equal('');
460
461             var selection = c.getSelection();
462             expect(selection.type).to.equal('caret');
463             expect(selection.element.sameNode(getTextElement('', c))).to.equal(true);
464             expect(selection.offset).to.equal(0);
465         });
466
467         it('splits its parent box if inside a span', function() {
468             var c = getCanvasFromXML('<section><div class="p">this <span>is</span> a paragraph</div></section>'),
469                 k = new Keyboard(c);
470
471             k.withCaret('i|s').press(K.ENTER);
472
473             var rootContents = c.wlxmlDocument.root.contents();
474
475             expect(rootContents.length).to.equal(2);
476
477             var p1 = rootContents[0],
478                 p2 = rootContents[1];
479
480             expect(p1.is({tagName: 'div', klass: 'p'})).to.equal(true);
481             expect(p2.is({tagName: 'div', klass: 'p'})).to.equal(true);
482
483             var p1Contents = p1.contents(),
484                 p2Contents = p2.contents();
485
486             expect(p1Contents[0].getText()).to.equal('this ');
487             expect(p1Contents[1].is({tagName: 'span'})).to.equal(true);
488             expect(p1Contents[1].contents()[0].getText()).to.equal('i');
489
490             
491             expect(p2Contents[0].is({tagName: 'span'})).to.equal(true);
492             expect(p2Contents[0].contents()[0].getText()).to.equal('s');
493             expect(p2Contents[1].getText()).to.equal(' a paragraph');
494
495             var selection = c.getSelection();
496             expect(selection.element.sameNode(getTextElement('s', c))).to.equal(true);
497             expect(selection.offset).to.equal(0);
498         });
499
500         it('splits its parent box if inside a double span', function() {
501             var c = getCanvasFromXML('<section><div class="p">this <span test="outer"><span test="inner">is</span></span> a paragraph</div></section>'),
502                 k = new Keyboard(c);
503
504             k.withCaret('i|s').press(K.ENTER);
505
506             var rootContents = c.wlxmlDocument.root.contents();
507
508             expect(rootContents.length).to.equal(2);
509
510             var p1 = rootContents[0],
511                 p2 = rootContents[1];
512
513             expect(p1.is({tagName: 'div', klass: 'p'})).to.equal(true);
514             expect(p2.is({tagName: 'div', klass: 'p'})).to.equal(true);
515
516             var p1Contents = p1.contents(),
517                 p2Contents = p2.contents();
518
519             /* first paragraph */
520             expect(p1Contents[0].getText()).to.equal('this ');
521             
522             var outer1 = p1Contents[1];
523             expect(outer1.getAttr('test')).to.equal('outer');
524             expect(outer1.contents().length).to.equal(1);
525             var inner1 = outer1.contents()[0];
526             expect(inner1.getAttr('test')).to.equal('inner');
527             expect(inner1.contents()[0].getText()).to.equal('i');
528
529             /* second paragraph */
530             var outer2 = p2Contents[0];
531             expect(outer2.getAttr('test')).to.equal('outer');
532             expect(outer2.contents().length).to.equal(1);
533             var inner2 = outer2.contents()[0];
534             expect(inner2.getAttr('test')).to.equal('inner');
535             expect(inner2.contents()[0].getText()).to.equal('s');
536
537             expect(p2Contents[1].getText()).to.equal(' a paragraph');
538
539             /* caret */
540             var selection = c.getSelection();
541             expect(selection.element.sameNode(getTextElement('s', c))).to.equal(true);
542             expect(selection.offset).to.equal(0);
543         });
544     });
545
546
547 });
548
549
550 });