const simple = require('simple-mock'); require('codemirror') require('../src/mergely'); const macbeth = require('./data/macbeth').join('\n'); const defaultOptions = { autoupdate: true, rhs_margin: 'right', wrap_lines: false, line_numbers: true, lcs: true, sidebar: true, viewport: false, ignorews: false, ignorecase: false, ignoreaccents: false, resize_timeout: 500, change_timeout: 50, lhs_cmsettings: {}, rhs_cmsettings: {}, bgcolor: '#eee', vpcolor: 'rgba(0, 0, 200, 0.5)', license: 'lgpl', cmsettings: { styleSelectedText: true, mode: null }, _debug: false }; describe('mergely', function () { let editor; let editorId = 0; function init(options) { editor = new window.Mergely(`#mergely-${editorId - 1}`, options); return editor; }; beforeEach(() => { const div = document.createElement('div'); div.id = `mergely-${editorId++}`; div.style.margin = '0px'; div.style.height = '275px'; document.querySelector('body').appendChild(div); }); afterEach(() => { if (editor._diffView.settings._debug) { // debugging, skip teardown return; } editor && editor.unbind(); simple.restore(); editor.el.parentNode.removeChild(editor.el); }); describe('initialization', () => { it('initializes without arguments', (done) => { const editor = init(); expect(editor).to.exist; editor.once('updated', () => { try { const { children } = editor.el.children[0]; const items = Array.from(children).map(a => a.className); // NOTE: if running karma debug, these tests can fail because // the debugger grabs the focus and the CodeMirror instance // loses `CodeMirror-focused` expect(items).to.deep.equal([ 'mergely-margin', 'mergely-column', 'CodeMirror cm-s-default CodeMirror-focused', 'mergely-canvas', 'mergely-column', 'CodeMirror cm-s-default', 'mergely-margin', 'mergely-splash' ]); expect(editor.get('lhs')).to.equal(''); expect(editor.get('rhs')).to.equal(''); done(); } catch (ex) { done(ex); } }); }); it('initializes with static arguments for lhs/rhs text with function', function (done) { const editor = init({ height: 100, license: 'lgpl-separate-notice', lhs: 'left-hand side text', rhs: 'right-hand side text' }); expect(editor).to.exist; editor.once('updated', () => { try { const { children } = editor.el.children[0]; const items = Array.from(children).map(a => a.className); // NOTE: if running karma debug, these tests can fail because // the debugger grabs the focus and the CodeMirror instance // loses `CodeMirror-focused` expect(items).to.deep.equal([ 'mergely-margin', 'mergely-column', 'CodeMirror cm-s-default CodeMirror-focused', 'mergely-canvas', 'mergely-column', 'CodeMirror cm-s-default', 'mergely-margin' ]); expect(editor.get('lhs')).to.equal('left-hand side text'); expect(editor.get('rhs')).to.equal('right-hand side text'); done(); } catch (ex) { done(ex); } }); }); it('initializes with static arguments for lhs/rhs text', function (done) { const editor = init({ height: 100, license: 'lgpl-separate-notice', lhs: 'left-hand side text', rhs: 'right-hand side text' }); expect(editor).to.exist; editor.once('updated', () => { try { const { children } = editor.el.children[0]; const items = Array.from(children).map(a => a.className); // NOTE: if running karma debug, these tests can fail because // the debugger grabs the focus and the CodeMirror instance // loses `CodeMirror-focused` expect(items).to.deep.equal([ 'mergely-margin', 'mergely-column', 'CodeMirror cm-s-default CodeMirror-focused', 'mergely-canvas', 'mergely-column', 'CodeMirror cm-s-default', 'mergely-margin' ]); expect(editor.get('lhs')).to.equal('left-hand side text'); expect(editor.get('rhs')).to.equal('right-hand side text'); done(); } catch (ex) { done(ex); } }); }); it('initializes with no sidebar', function (done) { const editor = init({ height: 100, license: 'lgpl-separate-notice', sidebar: false }); expect(editor).to.exist; editor.once('updated', () => { try { const { children } = editor.el.children[0]; const items = Array.from(children).map(a => a.className); // NOTE: if running karma debug, these tests can fail because // the debugger grabs the focus and the CodeMirror instance // loses `CodeMirror-focused` expect(items).to.deep.equal([ 'mergely-margin', 'mergely-column', 'CodeMirror cm-s-default CodeMirror-focused', 'mergely-canvas', 'mergely-column', 'CodeMirror cm-s-default', 'mergely-margin' ]); expect(children[0].style.visibility).to.equal('hidden'); expect(children[6].style.visibility).to.equal('hidden'); done(); } catch (ex) { done(ex); } }); }); it('initializes with options', function () { const initOptions = { fgcolor: { a: 'red', c: 'green', d: 'blue' }, ignorews: true, ignorecase: true, ignoreaccents: true, resize_timeout: 99, change_timeout: 99, fadein: 'slow', height: '100px', width: '400px', cmsettings: { lineSeparator: '\n', readOnly: true }, rhs_cmsettings: { readOnly: false } }; const editor = init({ ...initOptions }); const options = editor.options(); expect(options).to.deep.include({ ...initOptions }); }); it('initializes with and set lhs/rhs on update', function (done) { const editor = init(); expect(editor).to.exist; const test = () => { done(); }; editor.once('updated', () => { editor.on('updated', test); editor.lhs('left-hand side text'); editor.rhs('right-hand side text'); }); }); }); describe('clear', () => { it('should clear lhs side', function (done) { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', lhs: (setValue) => setValue('left-hand side text'), rhs: (setValue) => setValue('right-hand side text') }); const test = () => { try { expect(editor.get('lhs')).to.equal(''); expect(editor.get('rhs')).to.equal('right-hand side text'); done(); } catch (ex) { done(ex); } }; editor.once('updated', () => { editor.clear('lhs'); test(); }); }); it('should clear rhs side', function (done) { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', lhs: 'left-hand side text', rhs: 'right-hand side text' }); const test = () => { try { expect(editor.get('lhs')).to.equal('left-hand side text'); expect(editor.get('rhs')).to.equal(''); done(); } catch (ex) { done(ex); } }; editor.once('updated', () => { editor.clear('rhs'); test(); }); }); }); describe('cm', () => { it('should get CodeMirror from lhs and rhs sides', function (done) { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', lhs: 'left-hand 2 side text', rhs: 'right-hand 2 side text' }); editor.once('updated', () => { const lcm = editor.cm('lhs'); const rcm = editor.cm('rhs'); expect(lcm).to.not.equal(rcm); expect(lcm.getValue()).to.equal('left-hand 2 side text'); expect(rcm.getValue()).to.equal('right-hand 2 side text'); done(); }); }); }); describe('get', () => { it('should get lhs and rhs text', function () { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', lhs: 'left-hand side text', rhs: 'right-hand side text' }); editor.once('updated', () => { expect(editor.get('lhs')).to.equal('left-hand side text'); expect(editor.get('rhs')).to.equal('right-hand side text'); }); }); }); describe('lhs', () => { it('should set lhs value', function (done) { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', lhs: 'left-hand side text' }); const test = () => { try { expect(editor.get('lhs')).to.equal('banana'); done(); } catch (ex) { done(ex); } }; editor.once('updated', () => { expect(editor.get('lhs')).to.equal('left-hand side text'); editor.lhs('banana'); test(); }); }); }); describe('rhs', () => { it('should set rhs value', function (done) { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', rhs: 'right-hand side text' }); const test = () => { try { expect(editor.get('rhs')).to.equal('banana'); done(); } catch (ex) { done(ex); } }; editor.once('updated', () => { expect(editor.get('rhs')).to.equal('right-hand side text'); editor.rhs('banana'); test(); }); }); }); describe('merge', () => { it('should merge lhs to rhs', function (done) { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', lhs: 'left-hand side text', rhs: 'right-hand side text' }); const test = () => { try { expect(editor.get('lhs')).to.equal('left-hand side text'); expect(editor.get('rhs')).to.equal('left-hand side text'); const diff = editor.diff(); expect(diff).to.equal(''); done(); } catch (ex) { done(ex); } }; editor.once('updated', () => { expect(editor.get('lhs')).to.equal('left-hand side text'); expect(editor.get('rhs')).to.equal('right-hand side text'); editor.merge('rhs'); test(); }); }); it('should merge rhs to lhs', function (done) { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', lhs: 'left-hand side text', rhs: 'right-hand side text' }); const test = () => { try { expect(editor.get('lhs')).to.equal('right-hand side text'); expect(editor.get('rhs')).to.equal('right-hand side text'); const diff = editor.diff(); expect(diff).to.equal(''); done(); } catch (ex) { done(ex); } }; editor.once('updated', () => { expect(editor.get('lhs')).to.equal('left-hand side text'); expect(editor.get('rhs')).to.equal('right-hand side text'); editor.merge('lhs'); test(); }); }); }); describe('options', () => { it('should have default options', function (done) { const editor = init(); const test = () => { try { const currentOptions = editor.options(); expect(currentOptions).to.deep.equal({ ...defaultOptions, lhs: currentOptions.lhs, rhs: currentOptions.rhs }); done(); } catch (ex) { done(ex); } }; editor.once('updated', () => { currentOptions = editor.options(); editor.once('updated', test); editor.options({}); test(); }); }); it('should not change any options if set with empty object', function (done) { let currentOptions; const editor = init({ change_timeout: 0, license: 'lgpl-separate-notice', lhs: 'left-hand side text', rhs: 'right-hand side text' }); const test = () => { try { const newOptions = editor.options(); expect(currentOptions).to.deep.equal(newOptions); done(); } catch (ex) { done(ex); } }; editor.once('updated', () => { currentOptions = editor.options(); editor.once('updated', test); editor.options({}); test(); }); }); it('should change options', function (done) { let currentOptions; const editor = init({ change_timeout: 0, license: 'lgpl-separate-notice', lhs: 'left-hand side text', rhs: 'right-hand side text' }); const changes = { autoresize: false, fadein: 'slow', ignorews: true, ignorecase: true, ignoreaccents: true, viewport: true, wrap_lines: true, sidebar: false, line_numbers: false }; const test = () => { try { const newOptions = editor.options(); expect(newOptions).to.deep.equal({ ...currentOptions, ...changes }); done(); } catch (ex) { done(ex); } }; editor.once('updated', () => { currentOptions = editor.options(); editor.once('updated', test); editor.options(changes); test(); }); }); it('should ignore white-space', function (done) { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', ignorews: true, lhs: '\tthis is text', rhs: 'this\tis\ttext\t\t' }); editor.once('updated', () => { try { expect(editor.diff()).to.equal(''); expect(editor.el.querySelectorAll('.mergely.rhs.c')).to.have.length(0); expect(editor.el.querySelectorAll('.mergely.lhs.c')).to.have.length(0); done(); } catch (ex) { done(ex); } }); }); it('should ignore case', function (done) { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', ignorecase: true, lhs: 'thIS IS text', rhs: 'this is text' }); editor.once('updated', () => { try { expect(editor.diff()).to.equal(''); expect(editor.el.querySelectorAll('.mergely.rhs.c')).to.have.length(0); expect(editor.el.querySelectorAll('.mergely.lhs.c')).to.have.length(0); done(); } catch (ex) { done(ex); } }); }); it('should ignore accented characters', function (done) { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', ignoreaccents: true, lhs: 'comunicação', rhs: 'comunicacao' }); editor.once('updated', () => { try { expect(editor.diff()).to.equal(''); expect(editor.el.querySelectorAll('.mergely.rhs.c')).to.have.length(0); expect(editor.el.querySelectorAll('.mergely.lhs.c')).to.have.length(0); done(); } catch (ex) { done(ex); } }); }); it('should ignore white-space, case, and accented characters', function (done) { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', ignorews: true, ignoreaccents: true, ignorecase: true, lhs: 'This is Comunicação', rhs: '\t\tthis\tis\tcomunicacao' }); editor.once('updated', () => { try { expect(editor.diff()).to.equal(''); expect(editor.el.querySelectorAll('.mergely.rhs.c')).to.have.length(0); expect(editor.el.querySelectorAll('.mergely.lhs.c')).to.have.length(0); done(); } catch (ex) { done(ex); } }); }); }); describe('resize', () => { it('should trigger update on resize', function (done) { const loaded = simple.stub(); const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', loaded }); editor.once('updated', () => { editor.once('updated', () => { done(); }); editor.resize(); }); }); }); describe('scrollTo', () => { it('should scroll to line when not in view', function (done) { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', lhs: macbeth, rhs: macbeth.replace(/place/g, 'space') }); const test = () => { try { const { _isChangeInView: isVisible, _getViewportSide: getViewport, changes } = editor._diffView; let vp = getViewport.call(editor._diffView, 'lhs'); expect(vp).to.deep.equal({ from: 0, to: 15 }); const change = changes[4]; expect(isVisible('lhs', vp, change)).to.be.false; expect(isVisible('rhs', vp, change)).to.be.false; editor.once('updated', () => { try { vp = getViewport.call(editor._diffView, 'lhs'); expect(vp).to.deep.equal({ from: 687, to: 703 }); expect(isVisible('lhs', vp, change)).to.be.true; expect(isVisible('rhs', vp, change)).to.be.true; done(); } catch (ex) { done(ex); } }); editor.scrollTo('lhs', 696); } catch (ex) { done(ex); } }; editor.once('updated', () => { expect(editor.diff()).to.include('696c696'); test(); }); }); }); describe('scrollToDiff', () => { it('should scroll next 4 times and scroll into view', function (done) { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', lhs: macbeth, rhs: macbeth.replace(/place/g, 'space') }); let count = 0; const test = () => { count += 1; if (count <= 4) { editor.scrollToDiff('next'); return; } try { const { _isChangeInView: isVisible, _getViewportSide: getViewport, changes } = editor._diffView; const change = changes[3]; vp = getViewport.call(editor._diffView, 'lhs'); expect(vp).to.deep.equal({ from: 673, to: 689 }); expect(isVisible('lhs', vp, change)).to.be.true; expect(isVisible('rhs', vp, change)).to.be.true; done(); } catch (ex) { done(ex); } }; // intentional `editor.on` editor.on('updated', () => { expect(editor.diff()).to.include('696c696'); test(); }); }); }); describe('search', () => { it('should search and scroll to match', function (done) { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', lhs: macbeth, rhs: macbeth.replace(/place/g, 'space') }); let count = 0; const test = () => { try { const { _isChangeInView: isVisible, _getViewportSide: getViewport, changes } = editor._diffView; const change = changes[4]; const vp = getViewport.call(editor._diffView, 'lhs'); expect(vp).to.deep.equal({ from: 325, to: 340 }); done(); } catch (ex) { done(ex); } }; editor.once('updated', () => { editor.search('lhs', 'knock'); test(); }); }); }); describe('swap', () => { it('should swap sides', (done) => { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', lhs: 'left-hand side text', rhs: 'right-hand side text' }); const test = () => { try { expect(editor.get('lhs')).to.equal('right-hand side text'); expect(editor.get('rhs')).to.equal('left-hand side text'); done(); } catch (ex) { done(ex); } }; editor.once('updated', () => { editor.once('updated', test); editor.swap(); }); }); }); describe('summary', () => { it('should return empty', (done) => { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice' }); const test = () => { try { const summary = editor.summary(); expect(summary).to.deep.equal({ numChanges: 0, lhsLength: 0, rhsLength: 0, c: 0, a: 0, d: 0 }); done(); } catch (ex) { done(ex); } }; editor.once('updated', test); }); it('should return changes', (done) => { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', lhs: 'a\n\nb\nc\nd', rhs: 'aa\n\nd' }); const test = () => { try { const summary = editor.summary(); expect(summary).to.deep.equal({ numChanges: 2, lhsLength: 8, rhsLength: 5, c: 1, a: 0, d: 1 }); done(); } catch (ex) { done(ex); } }; editor.once('updated', test); }); }); describe('unmarkup', () => { it('should remove markup', (done) => { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', lhs: 'left-hand side text', rhs: 'right-hand side text' }); const test = () => { try { const found = document.querySelector('.mergely.ch.ind.lhs'); expect(found).to.be.null; done(); } catch (ex) { done(ex); } }; editor.once('updated', () => { try { const found = document.querySelector('.mergely.ch.ind.lhs'); expect(found).to.not.be.null; } catch (ex) { return done(ex); } editor.once('updated', test); editor.unmarkup(); }); }); }); describe('update', () => { it('should trigger update on options', (done) => { const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', lhs: 'left-hand side text', rhs: 'right-hand side text' }); editor.once('updated', () => { editor.once('updated', () => done()); editor.options({ wrap_lines: false }); }); }); it('should trigger update on lhs/rhs change', (done) => { const editor = init({ lhs: 'left-hand side text', rhs: 'right-hand side text' }); editor.once('updated', () => { editor.once('updated', () => done()); editor.lhs('lhs text'); editor.rhs('rhs text'); }); }); it('should trigger update on scroll', (done) => { const editor = init({ lhs: macbeth, rhs: macbeth.replace(/place/g, 'space') }); editor.once('updated', () => { editor.once('updated', () => done()); editor.scrollTo('lhs', 696); }); }); }); describe('_isChangeInView', () => { it('should be false when change less-than viewport', function (done) { const editor = init({ height: 100, change_timeout: 0, viewport: true, license: 'lgpl-separate-notice', lhs: macbeth, rhs: macbeth }); const test = () => { try { const { _isChangeInView: isVisible } = editor._diffView; expect(isVisible('lhs', {from: 10, to: 20}, { 'lhs-line-from': 0, 'lhs-line-to': 9 })).to.be.false; done(); } catch (ex) { done(ex); } } editor.once('updated', test); }); it('should be true when change less-than-equal viewport', function (done) { const editor = init({ height: 100, change_timeout: 0, viewport: true, license: 'lgpl-separate-notice', lhs: macbeth, rhs: macbeth }); const test = () => { try { const { _isChangeInView: isVisible } = editor._diffView; expect(isVisible('lhs', {from: 10, to: 20}, { 'lhs-line-from': 0, 'lhs-line-to': 10 })).to.be.true; done(); } catch (ex) { done(ex); } }; editor.once('updated', test); }); it('should be false when change greater-than viewport', function (done) { const editor = init({ height: 100, change_timeout: 0, viewport: true, license: 'lgpl-separate-notice', lhs: macbeth, rhs: macbeth }); const test = () => { try { const { _isChangeInView: isVisible } = editor._diffView; expect(isVisible('lhs', {from: 10, to: 20}, { 'lhs-line-from': 21, 'lhs-line-to': 22 })).to.be.false; done(); } catch (ex) { done(ex); } }; editor.once('updated', test); }); it('should be true when change straddles viewport from', function (done) { const editor = init({ height: 100, change_timeout: 0, viewport: true, license: 'lgpl-separate-notice', lhs: macbeth, rhs: macbeth }); const test = () => { try { const { _isChangeInView: isVisible } = editor._diffView; expect(isVisible('lhs', {from: 10, to: 20}, { 'lhs-line-from': 5, 'lhs-line-to': 11 })).to.be.true; done(); } catch (ex) { done(ex); } }; editor.once('updated', test); }); it('should be true when change straddles viewport to', function (done) { const editor = init({ height: 100, change_timeout: 0, viewport: true, license: 'lgpl-separate-notice', lhs: macbeth, rhs: macbeth }); const test = () => { try { const { _isChangeInView: isVisible } = editor._diffView; expect(isVisible('lhs', {from: 10, to: 20}, { 'lhs-line-from': 15, 'lhs-line-to': 21 })).to.be.true; done(); } catch (ex) { done(ex); } }; editor.once('updated', test); }); it('should be true when change encompasses viewport', function (done) { const editor = init({ height: 100, change_timeout: 0, viewport: true, license: 'lgpl-separate-notice', lhs: macbeth, rhs: macbeth }); const test = () => { try { const { _isChangeInView: isVisible } = editor._diffView; expect(isVisible('lhs', {from: 10, to: 20}, { 'lhs-line-from': 0, 'lhs-line-to': 25 })).to.be.true; done(); } catch (ex) { done(ex); } }; editor.once('updated', test); }); }); describe('security', () => { it('should not be vulnerable to XSS', function (done) { const xss = ''; const editor = init({ height: 100, change_timeout: 0, license: 'lgpl-separate-notice', lhs: xss, rhs: '' }); editor.once('updated', () => { // the value is as expected expect(editor.get('lhs')).to.equal(xss); // yet, shouldn't find injected script in DOM const found = document.querySelector('#injected'); done(found !== null); }); }); }); });