From f05e06232a80fc2ba59e51a7d3dff0644d8ada54 Mon Sep 17 00:00:00 2001 From: Jamie Peabody Date: Sat, 18 Dec 2021 15:56:17 +0000 Subject: [PATCH] refactor: BREAKING: 5.0.0-rc0 --- .babelrc.js | 9 +- examples/ajax.html | 7 +- examples/app.js | 33 +- examples/simple.html | 3 - examples/size.html | 17 +- karma.conf.js | 6 +- package.json | 28 +- src/diff-view.js | 102 ++- src/mergely.js | 90 ++- {tests => test}/data/macbeth.js | 0 test/mergely.spec.js | 1297 +++++++++++++++++++++++++++++++ tests/mergely.spec.js | 714 ----------------- webpack.dev.js | 7 +- webpack.prod.js | 1 - 14 files changed, 1471 insertions(+), 843 deletions(-) rename {tests => test}/data/macbeth.js (100%) create mode 100644 test/mergely.spec.js delete mode 100644 tests/mergely.spec.js diff --git a/.babelrc.js b/.babelrc.js index def46d0..0e27bfb 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -2,7 +2,7 @@ module.exports = function(api) { return { presets: [ [ - "@babel/preset-env", + '@babel/preset-env', { targets: { chrome: 59, @@ -11,9 +11,10 @@ module.exports = function(api) { ie: 11 }, // for uglifyjs... - forceAllTransforms: api.env("production"), - }, - ], + forceAllTransforms: api.env('production') + } + ] ], + retainLines: true }; }; diff --git a/examples/ajax.html b/examples/ajax.html index 329931b..e3ac86d 100644 --- a/examples/ajax.html +++ b/examples/ajax.html @@ -11,9 +11,6 @@ This example demonstrates how to set left and right editors using ajax. - - - @@ -139,8 +136,8 @@ This example demonstrates how to set left and right editors using ajax.
- Changes: 0 added lines, - 0 deleted lines, + Changes: 0 added lines, + 0 deleted lines, 0 changed lines
diff --git a/examples/app.js b/examples/app.js index 34362e7..a65b4a3 100644 --- a/examples/app.js +++ b/examples/app.js @@ -2,24 +2,25 @@ require('codemirror/addon/selection/mark-selection.js'); require('codemirror/lib/codemirror.css'); require('../src/mergely.css'); -var lhs = `\ +const lhs = `\ the quick red fox jumped over the hairy dog `; -var rhs = `\ +const rhs = `\ the quick brown fox jumped over the lazy dog `; -$(document).ready(function () { - console.log('here', document.innerHTML); +document.onreadystatechange = function () { + if (document.readyState !== 'complete') { + return; + } - window.mergely('#mergely', { + const doc = new Mergely('#mergely', { license: 'lgpl', ignorews: true, wrap_lines: true, - //_debug: 'scroll', cmsettings: { readOnly: false }, @@ -30,22 +31,4 @@ $(document).ready(function () { setValue(rhs); } }); - - /* - $('#mergely').mergely({ - license: 'lgpl', - ignorews: true, - wrap_lines: true, - //_debug: 'scroll', - cmsettings: { - readOnly: false - }, - lhs: function(setValue) { - setValue(lhs); - }, - rhs: function(setValue) { - setValue(rhs); - } - }); - */ -}); +}; diff --git a/examples/simple.html b/examples/simple.html index ba92126..ebdcd76 100644 --- a/examples/simple.html +++ b/examples/simple.html @@ -11,9 +11,6 @@ This example demonstrates the minimum amount of code required to use Mergely. - - - diff --git a/examples/size.html b/examples/size.html index d6aba9e..d7435e5 100755 --- a/examples/size.html +++ b/examples/size.html @@ -11,9 +11,6 @@ This example demonstrates how to enable line wrapping - - - @@ -31,7 +28,7 @@ This example demonstrates how to enable line wrapping height: 200, license: 'lgpl-separate-notice', cmsettings: { - readOnly: false, + readOnly: false, lineWrapping: true, } }); @@ -40,7 +37,7 @@ This example demonstrates how to enable line wrapping height: 200, license: 'lgpl-separate-notice', cmsettings: { - readOnly: false, + readOnly: false, lineWrapping: true, } }); @@ -49,7 +46,7 @@ This example demonstrates how to enable line wrapping height: 200, license: 'lgpl-separate-notice', cmsettings: { - readOnly: false, + readOnly: false, lineWrapping: true, } }); @@ -58,7 +55,7 @@ This example demonstrates how to enable line wrapping height: 200, license: 'lgpl-separate-notice', cmsettings: { - readOnly: false, + readOnly: false, lineWrapping: true, } }); @@ -68,11 +65,11 @@ This example demonstrates how to enable line wrapping autoresize: false, license: 'lgpl-separate-notice', cmsettings: { - readOnly: false, + readOnly: false, lineWrapping: true, } }); - + $.ajax({ type: 'GET', async: true, dataType: 'text', url: 'lhs.txt', @@ -111,6 +108,6 @@ This example demonstrates how to enable line wrapping
- + diff --git a/karma.conf.js b/karma.conf.js index b181102..1293c91 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -17,17 +17,17 @@ module.exports = function(config) { files: [ 'node_modules/codemirror/lib/codemirror.css', 'src/mergely.css', - 'tests/**/*.spec.js' + 'test/**/*.spec.js' ], preprocessors: { - 'node_modules/jquery/dist/jquery.js': ['webpack'], 'node_modules/codemirror/lib/codemirror.js': ['webpack'], - 'tests/**/*.spec.js': ['webpack'], + 'test/**/*.spec.js': ['webpack'], 'src/**/*.js': ['webpack'] }, reporters: ['mocha'], + mochaReporter: { showDiff: true }, diff --git a/package.json b/package.json index c31cfd2..2bb5d63 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,16 @@ { "name": "mergely", - "version": "4.3.8", + "version": "5.0.0-rc0", "description": "A javascript UI for diff/merge", - "directories": { - "doc": "doc", - "example": "examples", - "test": "test" + "license": "(GPL-3.0 OR LGPL-3.0 OR MPL-1.1 OR SEE LICENSE IN LICENSE)", + "author": { + "name": "Jamie Peabody", + "email": "jamie.peabody@gmail.com", + "url": "https://mergely.com" + }, + "homepage": "https://github.com/wickedest/Mergely#readme", + "bugs": { + "url": "https://github.com/wickedest/Mergely/issues" }, "repository": { "type": "git", @@ -23,16 +28,6 @@ "diff", "myers" ], - "author": { - "name": "Jamie Peabody", - "email": "jamie.peabody@gmail.com", - "url": "http://mergely.com" - }, - "license": "(GPL-3.0 OR LGPL-3.0 OR MPL-1.1 OR SEE LICENSE IN LICENSE)", - "bugs": { - "url": "https://github.com/wickedest/Mergely/issues" - }, - "homepage": "https://github.com/wickedest/Mergely#readme", "devDependencies": { "@babel/core": "^7.1.6", "@babel/preset-env": "^7.1.6", @@ -47,15 +42,14 @@ "css-loader": "^5.0.0", "git-conventional-commits": "^1.1.0", "html-webpack-plugin": "^4.5.0", - "jquery": "^3.5.1", "karma": "^5.2.3", "karma-chai": "^0.1.0", "karma-chrome-launcher": "^3.1.0", - "karma-coverage-istanbul-reporter": "^1.3.0", "karma-mocha": "^2.0.1", "karma-mocha-reporter": "^2.2.5", "karma-webpack": "^4.0.2", "mocha": "^8.1.3", + "simple-mock": "^0.8.0", "standard-version": "^9.3.2", "style-loader": "^2.0.0", "webpack": "^4.44.2", diff --git a/src/diff-view.js b/src/diff-view.js index 8265a31..bc4dad2 100644 --- a/src/diff-view.js +++ b/src/diff-view.js @@ -1,3 +1,6 @@ +require('codemirror/addon/search/searchcursor.js'); +require('codemirror/addon/selection/mark-selection.js'); + const Timer = require('./timer'); const diff = require('./diff'); const DiffParser = require('./diff-parser'); @@ -7,15 +10,23 @@ const LCS = require('./lcs'); CHANGES: BREAKING: +Removed dependency on `jQuery`. Added `.mergely-editor` to the DOM to scope all the CSS changes. CSS now prefixes `.mergely-editor`. Current active change gutter line number style changed from `.CodeMirror-linenumber` to `.CodeMirror-gutter-background`. Removed support for jquery-ui merge buttons. +API switched from jQuery style to object methods. +No longer necessary to separately require codemirror/addon/search/searchcursor +No longer necessary to separately require codemirror/addon/selection/mark-selection FEATURE: Gutter click now scrolls to any line. FIX: Fixed issue where canvas markup was not rendered when `viewport` enabled. +Fixed timing issue where swap sides may not work as expected. +Fixed issue where unmarkup did not emit an updated event. +Fixed issue where init triggered an updated event when autoupdate is disabled. +Fixed documentation issue where `merge` incorrectly stated: from the specified `side` to the opposite side */ const MERGELY_ICON = 'data:image/png;base64,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'; @@ -26,20 +37,18 @@ const NOTICES = [ 'commercial' ]; -function CodeMirrorDiffView(el, options, { jQuery, CodeMirror }) { +function CodeMirrorDiffView(el, options, { CodeMirror }) { CodeMirror.defineExtension('centerOnCursor', function() { const coords = this.cursorCoords(null, 'local'); this.scrollTo(null, (coords.top + coords.bottom) / 2 - (this.getScrollerElement().clientHeight / 2)); }); - this.jQuery = jQuery; this.CodeMirror = CodeMirror; this.init(el, options); }; CodeMirrorDiffView.prototype.init = function(el, options = {}) { - const { jQuery } = this; this.settings = { autoupdate: true, autoresize: true, @@ -123,7 +132,6 @@ CodeMirrorDiffView.prototype.init = function(el, options = {}) { ...options }; // save this element for faster queries - this.element = jQuery(el); this.el = el; this.lhs_cmsettings = { @@ -147,17 +155,21 @@ CodeMirrorDiffView.prototype.init = function(el, options = {}) { }; CodeMirrorDiffView.prototype.unbind = function() { - if (this.changed_timeout != null) clearTimeout(this.changed_timeout); + if (this.changed_timeout != null) { + clearTimeout(this.changed_timeout); + } this.editor.lhs.toTextArea(); this.editor.rhs.toTextArea(); + this._unbound = true; }; -CodeMirrorDiffView.prototype.destroy = function() { - this.teardown(); -}; - -CodeMirrorDiffView.prototype.teardown = function() { - this.unbind(); +CodeMirrorDiffView.prototype.remove = function() { + if (!this._unbound) { + this.unbind(); + } + while (this.el.lastChild) { + this.el.removeChild(this.el.lastChild); + } }; CodeMirrorDiffView.prototype.lhs = function(text) { @@ -175,11 +187,13 @@ CodeMirrorDiffView.prototype.rhs = function(text) { }; CodeMirrorDiffView.prototype.update = function() { - this._changing(); + this._changing({ force: true }); }; CodeMirrorDiffView.prototype.unmarkup = function() { this._clear(); + this.trace('change', 'emit: updated'); + this.el.dispatchEvent(new Event('updated')); }; CodeMirrorDiffView.prototype.scrollToDiff = function(direction) { @@ -213,6 +227,7 @@ CodeMirrorDiffView.prototype.mergeCurrentChange = function(side) { }; CodeMirrorDiffView.prototype.scrollTo = function(side, num) { + this.trace('scroll', 'scrollTo', side, num); const ed = this.editor[side]; ed.setCursor(num); ed.centerOnCursor(); @@ -223,38 +238,39 @@ CodeMirrorDiffView.prototype._setOptions = function(opts) { ...this.settings, ...opts }; - if (this.settings.hasOwnProperty('rhs_margin')) { - // dynami259*-6+cally swap the margin - if (this.settings.rhs_margin == 'left') { - this.element.find('.mergely-margin:last-child').insertAfter( - this.element.find('.mergely-canvas')); - } - else { - const target = this.element.find('.mergely-margin').last(); - target.appendTo(target.parent()); - } - } if (this.settings.hasOwnProperty('sidebar')) { // dynamically enable sidebars if (this.settings.sidebar) { - this.element.find('.mergely-margin').css({display: 'block'}); + const divs = document.querySelectorAll('.mergely-margin'); + for (const div of divs) { + div.style.visibility = 'visible'; + } } else { - this.element.find('.mergely-margin').css({display: 'none'}); + const divs = document.querySelectorAll('.mergely-margin'); + for (const div of divs) { + div.style.visibility = 'hidden'; + } } } // if options set after init if (this.editor) { const le = this.editor.lhs; const re = this.editor.rhs; - if (this.settings.hasOwnProperty('wrap_lines')) { + if (opts.hasOwnProperty('wrap_lines')) { le.setOption('lineWrapping', this.settings.wrap_lines); re.setOption('lineWrapping', this.settings.wrap_lines); } - if (this.settings.hasOwnProperty('line_numbers')) { + if (opts.hasOwnProperty('line_numbers')) { le.setOption('lineNumbers', this.settings.line_numbers); re.setOption('lineNumbers', this.settings.line_numbers); } + if (opts.hasOwnProperty('rhs_margin')) { + // dynamically swap the margin + const divs = document.querySelectorAll('.mergely-editor > div'); + // [0:margin] [1:lhs] [2:mid] [3:rhs] [4:margin], swaps 4 with 3 + divs[4].parentNode.insertBefore(divs[4], divs[3]); + } } }; @@ -275,14 +291,16 @@ CodeMirrorDiffView.prototype.swap = function() { } const le = this.editor.lhs; const re = this.editor.rhs; - re.setValue(le.getValue()); - le.setValue(re.getValue()); + const lv = le.getValue(); + const rv = re.getValue(); + re.setValue(lv); + le.setValue(rv); }; CodeMirrorDiffView.prototype.merge = function(side) { const le = this.editor.lhs; const re = this.editor.rhs; - if (side == 'lhs' && !this.lhs_cmsettings.readOnly) { + if (side === 'lhs' && !this.lhs_cmsettings.readOnly) { le.setValue(re.getValue()); } else if (!this.rhs_cmsettings.readOnly) { re.setValue(le.getValue()); @@ -332,6 +350,9 @@ CodeMirrorDiffView.prototype.cm = function(side) { CodeMirrorDiffView.prototype.search = function(side, query, direction) { const editor = this.editor[side]; + if (!editor.getSearchCursor) { + throw new Error('install CodeMirror search addon'); + } const searchDirection = (direction === 'prev') ? 'findPrevious' : 'findNext'; const start = { line: 0, ch: 0 }; @@ -424,7 +445,11 @@ CodeMirrorDiffView.prototype.bind = function(el) { el.append(canvasRhs); } if (!this.settings.sidebar) { - el.find('.mergely-margin').css({ display: 'none' }); + // it would be better if this just used this.options() + const divs = document.querySelectorAll('.mergely-margin'); + for (const div of divs) { + div.style.visibility = 'hidden'; + } } if (NOTICES.indexOf(this.settings.license) < 0) { const noticeTypes = { @@ -532,7 +557,6 @@ CodeMirrorDiffView.prototype.bind = function(el) { // scrollToDiff() from gutter function gutterClicked(side, line, ev) { - console.log('gutter', ev.target) if (ev.target.className.includes('merge-button')) { ev.preventDefault(); return; @@ -686,7 +710,7 @@ CodeMirrorDiffView.prototype._scrolling = function({ side, id }) { this.trace('scroll', 'scroll', scroll); if (scroll || force_scroll) { // scroll the other side - this.trace('scroll', 'scrolling other side', top_to - top_adjust); + this.trace('scroll', 'scrolling other side to pos:', top_to - top_adjust); // disable next scroll event because we trigger it this._skipscroll[oside] = true; this.editor[oside].scrollTo(left_to, top_to - top_adjust); @@ -707,10 +731,14 @@ CodeMirrorDiffView.prototype._scrolling = function({ side, id }) { this.trace('scroll', 'scrolled'); }; -CodeMirrorDiffView.prototype._changing = function() { +CodeMirrorDiffView.prototype._changing = function({ force } = { force: false }) { this.trace('change', 'changing-timeout', this.changed_timeout); if (this.changed_timeout != null) clearTimeout(this.changed_timeout); this.changed_timeout = setTimeout(() => { + if (!force && !this.settings.autoupdate) { + this.trace('change', 'ignore', force, this.settings.autoupdate); + return; + } Timer.start(); this._changed(); this.trace('change', 'total time', Timer.stop()); @@ -799,7 +827,6 @@ CodeMirrorDiffView.prototype._diff = function() { this.trace('change', 'markup time', Timer.stop()); this._draw_diff(this.changes); this.trace('change', 'draw time', Timer.stop()); - this.el.dispatchEvent(new Event('updated')); }; CodeMirrorDiffView.prototype._get_viewport_side = function(side) { @@ -1434,17 +1461,18 @@ CodeMirrorDiffView.prototype._draw_diff = function(changes) { this._handleLhsMarginClick = function (ev) { const y = ev.pageY - ex.lhs_xyoffset.top - (lto / 2); const sto = Math.max(0, (y / mcanvas_lhs.height) * ex.lhs_scroller.scrollHeight); - console.log('scroll', sto); ex.lhs_scroller.scrollTo({ top: sto }); }; this._handleRhsMarginClick = function (ev) { const y = ev.pageY - ex.rhs_xyoffset.top - (rto / 2); const sto = Math.max(0, (y / mcanvas_rhs.height) * ex.rhs_scroller.scrollHeight); - console.log('scroll', sto); ex.rhs_scroller.scrollTo({ top: sto }); }; ex.lhs_margin.addEventListener('click', this._handleLhsMarginClick); ex.rhs_margin.addEventListener('click', this._handleRhsMarginClick); + + this.trace('change', 'emit: updated'); + this.el.dispatchEvent(new Event('updated')); }; CodeMirrorDiffView.prototype.trace = function(name) { diff --git a/src/mergely.js b/src/mergely.js index acd558c..f50a881 100644 --- a/src/mergely.js +++ b/src/mergely.js @@ -1,23 +1,79 @@ -"use strict"; - -(function(jQuery, CodeMirror) { - const CodeMirrorDiffView = require('./diff-view'); -const Mergely = function() { }; +class Mergely { + constructor(element, options) { + this._diffView = new CodeMirrorDiffView(element, options, { + CodeMirror + }); + this.el = element; + this._diffView.bind(element); + const methods = [ + 'clear', + 'cm', + 'diff', + 'get', + 'lhs', + 'merge', + 'mergeCurrentChange', + 'options', + 'remove', + 'resize', + 'rhs', + 'scrollTo', + 'scrollToDiff', + 'search', + 'summary', + 'swap', + 'unmarkup', + 'update' + ]; + for (const method of methods) { + this[method] = this._diffView[method].bind(this._diffView); + } + this._listeners = []; + this.addEventListener = element.addEventListener.bind(element); + this.removeEventListener = element.removeEventListener.bind(element); + } -Mergely.prototype.name = 'mergely'; + unbind() { + for (const [ event, listener ] of this._listeners) { + this.removeEventListener(event, listener); + } + this._diffView.unbind(); + delete this._diffView; + } -Mergely.prototype.init = function(el, options) { - this.diffView = new CodeMirrorDiffView(el, options, { jQuery, CodeMirror }); - this.bind(el); -}; + /** + * @deprecated + */ + mergelyUnregister() { + } -Mergely.prototype.bind = function(el) { - this.diffView.bind(el); -}; + on(event, listener) { + this._listeners.push([ event, listener ]); + this.addEventListener(event, listener); + } -window.mergely = function (selector, options = {}) { + once(event, listener) { + this._listeners.push([ event, listener ]); + this.addEventListener(event, listener, { once: true }); + } + + /** + * @deprecated + * @param {*} method + * @param {...any} options + */ + mergely(method, ...options) { + this[method](...options); + } + + remove() { + + } +} + +window.Mergely = function (selector, options = {}) { let element = selector; if (typeof selector === 'string') { element = document.querySelector(selector); @@ -27,8 +83,6 @@ window.mergely = function (selector, options = {}) { } else if (!element) { throw new Error('Element cannot be null'); } - const mergely = new Mergely(); - mergely.init(element, options); + const mergely = new Mergely(element, options); + return mergely; }; - -})(require('jquery'), require('CodeMirror')); diff --git a/tests/data/macbeth.js b/test/data/macbeth.js similarity index 100% rename from tests/data/macbeth.js rename to test/data/macbeth.js diff --git a/test/mergely.spec.js b/test/mergely.spec.js new file mode 100644 index 0000000..cb52758 --- /dev/null +++ b/test/mergely.spec.js @@ -0,0 +1,1297 @@ +const CodeMirror = require('CodeMirror'); +const simple = require('simple-mock'); +const Mergely = require('../src/mergely'); +const macbeth = require('./data/macbeth').join('\n'); + +const defaultOptions = { + autoupdate: true, + autoresize: true, + rhs_margin: 'right', + wrap_lines: false, + line_numbers: true, + lcs: true, + sidebar: true, + viewport: false, + ignorews: false, + ignorecase: false, + ignoreaccents: false, + fadein: 'fast', + resize_timeout: 500, + change_timeout: 150, + fgcolor: { + a: '#4ba3fa', + c: '#a3a3a3', + d: '#ff7f7f', + ca: '#4b73ff', + cc: '#434343', + cd: '#ff4f4f' + }, + bgcolor: '#eee', + vpcolor: 'rgba(0, 0, 200, 0.5)', + license: '', + width: 'auto', + height: 'auto', + cmsettings: { + styleSelectedText: true + } +}; + +describe('mergely', function () { + let editor; + function init(options) { + const div = document.createElement('div'); + div.id = 'mergely'; + div.style.margin = '0px'; + document.querySelector('body').appendChild(div); + editor = new window.Mergely('#mergely', options); + return editor; + }; + + afterEach(() => { + editor.remove(); + simple.restore(); + }); + + describe('initialization', () => { + it('initializes without arguments', () => { + const editor = init({ + height: 100, + license: 'lgpl-separate-notice' + }); + expect(editor).to.exist; + const { children } = editor.el; + expect(children.length).to.equal(5); + expect(children[0].className).to.equal('mergely-margin'); + expect(children[1].className).to.equal('mergely-column'); + expect(children[2].className).to.equal('mergely-canvas'); + expect(children[3].className).to.equal('mergely-column'); + expect(children[4].className).to.equal('mergely-margin'); + expect(editor.get('lhs')).to.equal(''); + expect(editor.get('rhs')).to.equal(''); + }); + + it('initializes with static arguments for lhs/rhs text', function () { + const editor = init({ + height: 100, + license: 'lgpl-separate-notice', + lhs: (setValue) => setValue('left-hand side text'), + rhs: (setValue) => setValue('right-hand side text') + }); + expect(editor).to.exist; + const { children } = editor.el; + expect(children.length).to.equal(5); + expect(children[0].className).to.equal('mergely-margin'); + expect(children[1].className).to.equal('mergely-column'); + expect(children[2].className).to.equal('mergely-canvas'); + expect(children[3].className).to.equal('mergely-column'); + expect(children[4].className).to.equal('mergely-margin'); + expect(editor.get('lhs')).to.equal('left-hand side text'); + expect(editor.get('rhs')).to.equal('right-hand side text'); + }); + + it('initializes with no sidebar', function () { + const editor = init({ + height: 100, + license: 'lgpl-separate-notice', + sidebar: false + }); + expect(editor).to.exist; + const { children } = editor.el; + expect(children.length).to.equal(5); + expect(children[0].className).to.equal('mergely-margin'); + expect(children[0].style.visibility).to.equal('hidden'); + expect(children[1].className).to.equal('mergely-column'); + expect(children[2].className).to.equal('mergely-canvas'); + expect(children[3].className).to.equal('mergely-column'); + expect(children[4].className).to.equal('mergely-margin'); + expect(children[4].style.visibility).to.equal('hidden'); + }); + + it('initializes with default options', function () { + const editor = init(); + const options = editor.options(); + expect(options).to.deep.include({ + autoupdate: true, + autoresize: true, + rhs_margin: 'right', + wrap_lines: false, + line_numbers: true, + lcs: true, + sidebar: true, + viewport: false, + ignorews: false, + ignorecase: false, + ignoreaccents: false, + fadein: 'fast', + resize_timeout: 500, + change_timeout: 150, + fgcolor: { + a: '#4ba3fa', + c: '#a3a3a3', + d: '#ff7f7f', + ca: '#4b73ff', + cc: '#434343', + cd: '#ff4f4f' + }, + bgcolor: '#eee', + vpcolor: 'rgba(0, 0, 200, 0.5)', + license: 'lgpl', + width: 'auto', + height: 'auto', + cmsettings: { + styleSelectedText: true + }, + lhs_cmsettings: {}, + rhs_cmsettings: {} + }); + expect(options.lhs).to.be.a('function'); + expect(options.rhs).to.be.a('function'); + expect(options.loaded).to.be.a('function'); + expect(options.resize).to.be.a('function'); + expect(options.resized).to.be.a('function'); + }); + + it('initializes with options', function () { + const initOptions = { + autoupdate: false, + 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 + }); + }); + }); + + describe('clear', () => { + it('should clear lhs and rhs sides', 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(''); + const diff = editor.diff(); + expect(diff).to.equal(''); + done(); + } catch (ex) { + done(ex); + } + }; + editor.el.addEventListener('updated', test, { once: true }); + expect(editor.get('lhs')).to.equal('left-hand side text'); + expect(editor.get('rhs')).to.equal('right-hand side text'); + editor.clear('lhs'); + editor.clear('rhs'); + }); + }); + + describe('cm', () => { + it('should get CodeMirror from lhs and rhs sides', function () { + 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 lcm = editor.cm('lhs'); + const rcm = editor.cm('rhs'); + expect(lcm).to.not.equal(rcm); + expect(lcm.getValue()).to.equal('left-hand side text'); + expect(rcm.getValue()).to.equal('right-hand side text'); + }); + }); + + describe('get', () => { + it('should get lhs and rhs text', function () { + 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') + }); + 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: (setValue) => setValue('left-hand side text') + }); + const test = () => { + try { + expect(editor.get('lhs')).to.equal('banana'); + done(); + } catch (ex) { + done(ex); + } + }; + editor.el.addEventListener('updated', test, { once: true }); + expect(editor.get('lhs')).to.equal('left-hand side text'); + editor.lhs('banana'); + }); + }); + + describe('rhs', () => { + it('should set rhs value', function (done) { + const editor = init({ + height: 100, + change_timeout: 0, + license: 'lgpl-separate-notice', + rhs: (setValue) => setValue('right-hand side text') + }); + const test = () => { + try { + expect(editor.get('lhs')).to.equal('banana'); + done(); + } catch (ex) { + done(ex); + } + }; + editor.el.addEventListener('updated', test, { once: true }); + expect(editor.get('rhs')).to.equal('right-hand side text'); + editor.lhs('banana'); + }); + }); + + describe('merge', () => { + it('should merge lhs to rhs', 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('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.el.addEventListener('updated', test, { once: true }); + expect(editor.get('lhs')).to.equal('left-hand side text'); + expect(editor.get('rhs')).to.equal('right-hand side text'); + editor.merge('rhs'); + }); + + it('should merge rhs to lhs', 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('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.el.addEventListener('updated', test, { once: true }); + expect(editor.get('lhs')).to.equal('left-hand side text'); + expect(editor.get('rhs')).to.equal('right-hand side text'); + editor.merge('lhs'); + }); + }); + + describe('mergeCurrentChange', () => { + const opts = [{ // add 0-7 merge lhs + lhs: '', + rhs: '\na', + dir: 'lhs', + name: 'add is line 2 and insert into end of empty lhs' + }, + { + lhs: '', + rhs: 'a\n', + dir: 'lhs', + name: 'add is line 1 and insert into end of empty lhs' + }, + { + lhs: 'a', + rhs: 'a\nb', + dir: 'lhs', + name: 'add is line 2 and insert into end to existing lhs' + }, + { + lhs: 'a', + rhs: 'a\nbcd', + dir: 'lhs', + name: 'add is line 2 of length 3 and insert into end of existing lhs' + }, + { + lhs: 'a', + rhs: 'a\nb\nc\nd', + dir: 'lhs', + name: 'add is lines 2-4 and insert into end of existing lhs' + }, + { + lhs: '', + rhs: '\nabcd', + dir: 'lhs', + name: 'add is line 2 of length 4 and lhs is empty' + }, + { + lhs: 'abcd', + rhs: '\nabcd', + dir: 'lhs', + name: 'add is line 1 cr and insert into front of lhs' + }, + { + lhs: 'a\nd', + rhs: 'a\nb\nc\nd', + dir: 'lhs', + name: 'add is lines 2-3 and insert into middle of lhs' + }, + // add 8-15 merge rhs + { + lhs: '', + rhs: '\na', + dir: 'rhs', + name: 'add is line 2 and empty lhs will replace non-empty rhs' + }, + { + lhs: '', + rhs: 'a\n', + dir: 'rhs', + name: 'add is line 1 and empty lhs will replace line 1 of rhs' + }, + { + lhs: 'a', + rhs: 'a\nb', + dir: 'rhs', + name: 'add is line 2 and empty end lhs will replace line 2 of rhs' + }, + { + lhs: 'a', + rhs: 'a\nbcd', + dir: 'rhs', + name: 'add is line 2 of length 3 and empty end lhs will replace line 2 of rhs' + }, + { + lhs: 'a', + rhs: 'a\nb\nc\nd', + dir: 'rhs', + name: 'add is lines 2-4 and empty end lhs will replace last 3 lines of rhs' + }, + { + lhs: '', + rhs: '\nabcd', + dir: 'rhs', + name: 'add is line 2 of length 4 and empty lhs will replace last line of rhs' + }, + { + lhs: 'abcd', + rhs: '\nabcd', + dir: 'rhs', + name: 'add is line 1 cr and empty front will replace first line of rhs' + }, + { + lhs: 'a\nd', + rhs: 'a\nb\nc\nd', + dir: 'rhs', + name: 'add is lines 2-3 and empty middle will replace middle two lines of rhs' + }, + // change 16-21 merge lhs + { + lhs: 'a', + rhs: '', + dir: 'lhs', + name: 'change is line 1 and will replace empty rhs' + }, + { + lhs: 'a\nb\nc', + rhs: '', + dir: 'lhs', + name: 'change is lines 1-3 and will replace empty rhs' + }, + { + lhs: 'a', + rhs: 'b', + dir: 'lhs', + name: 'change is line 1 and will replace opposite line 1' + }, + { + lhs: 'a\nb', + rhs: 'c\nd', + dir: 'lhs', + name: 'change is lines 2 and will replace opposite line 2' + }, + { + lhs: 'a\nbc\nef', + rhs: 'a\nbcd\nef', + dir: 'lhs', + name: 'change is middle line 2 and will replace opposite middle line 2' + }, + { + lhs: 'a\nb\n\n', + rhs: 'a1\nb1\nc1\nd', + dir: 'lhs', + name: 'change is all lines 1-3 and will replace opposite all lines 1-4' + }, + // change 22-27 merge rhs + { + lhs: 'a', + rhs: '', + dir: 'rhs', + name: 'change is line 1 and will empty the rhs' + }, + { + lhs: 'a\nb\nc', + rhs: '', + dir: 'rhs', + name: 'change is lines 1-3 and will replace empty rhs' + }, + { + lhs: 'a', + rhs: 'b', + dir: 'rhs', + name: 'change is line 1 and will replace opposite rhs line 1' + }, + { + lhs: 'a\nb', + rhs: 'c\nd', + dir: 'rhs', + name: 'change is lines 2 and will replace opposite rhs line 2' + }, + { + lhs: 'a\nbc\nef', + rhs: 'a\nbcd\nef', + dir: 'rhs', + name: 'change is middle line 2 and will replace opposite rhs middle line 2' + }, + { + lhs: 'a\nb\n\n', + rhs: 'a1\nb1\nc1\nd', + dir: 'rhs', + name: 'change is all lines 1-3 and will replace opposite rhs all lines 1-4' + }, + // delete 28-35 merge lhs + { + lhs: '\na', + rhs: '', + dir: 'lhs', + name: 'delete is line 2 and will merge empty rhs' + }, + { + lhs: 'a\n', + rhs: '', + dir: 'lhs', + name: 'delete is line 1 and will merge empty rhs leaving cr on line 2' + }, + { + lhs: 'a\nb', + rhs: 'a', + dir: 'lhs', + name: 'delete is line 2 and will merge into end to existing rhs' + }, + { + lhs: 'a\nbcd', + rhs: 'a', + dir: 'lhs', + name: 'delete is line 2 of length 3 and insert into end of existing rhs' + }, + { + lhs: 'a\nb\nc\nd', + rhs: 'a', + dir: 'lhs', + name: 'delete is lines 2-4 and insert into end of existing rhs' + }, + { + lhs: '\nabcd', + rhs: '', + dir: 'lhs', + name: 'delete is line 2 of length 4 and insert into end of empty rhs' + }, + { + lhs: '\nabcd', + rhs: 'abcd', + dir: 'lhs', + name: 'delete is line 1 cr and insert into front of rhs' + }, + { + lhs: 'a\nb\nc\nd', + rhs: 'a\nd', + dir: 'lhs', + name: 'delete is lines 2-3 and insert into middle of rhs' + }, + { + lhs: 'a\nb\nc', + rhs: 'a\nb\nx\nc', + dir: 'lhs', + name: 'delete line 3 of length 1 of rhs and will be replaced with empty line 2 of lhs' + }, + // delete 36-43 merge rhs + { + lhs: '\na', + rhs: '', + dir: 'rhs', + name: 'delete is line 2 and will merge empty rhs' + }, + { + lhs: 'a\n', + rhs: '', + dir: 'rhs', + name: 'delete is line 1 and will merge empty rhs leaving cr on line 2' + }, + { + lhs: 'a\nb', + rhs: 'a', + dir: 'rhs', + name: 'delete is line 2 and will merge into end to existing rhs' + }, + { + lhs: 'a\nbcd', + rhs: 'a', + dir: 'rhs', + name: 'delete is line 2 of length 3 and insert into end of existing rhs' + }, + { + lhs: 'a\nb\nc\nd', + rhs: 'a', + dir: 'rhs', + name: 'delete is lines 2-4 and insert into end of existing rhs' + }, + { + lhs: '\nabcd', + rhs: '', + dir: 'rhs', + name: 'delete is line 2 of length 4 and insert into end of empty rhs' + }, + { + lhs: '\nabcd', + rhs: 'abcd', + dir: 'rhs', + name: 'delete is line 1 cr and insert into front of rhs' + }, + { + lhs: 'a\nb\nc\nd', + rhs: 'a\nd', + dir: 'rhs', + name: 'delete is lines 2-3 and insert into middle of rhs' + }, + { + lhs: 'a\nb\nc', + rhs: 'a\nb\nx\nc', + dir: 'rhs', + name: 'merge rhs line 3 of length 1 and insert after line 2 of lhs' + }, + { + lhs: '\ngood', + rhs: 'good', + dir: 'rhs', + name: 'lhs has deleted line at start' + }, + { + lhs: '\na\nb\nc\ngood', + rhs: 'good', + dir: 'rhs', + name: 'lhs has multiple deleted lines at start' + }, + { + lhs: 'good\n', + rhs: 'good', + dir: 'rhs', + name: 'lhs has deleted line at end (issue #115)' + }, + { + lhs: 'good\na\nb\nc', + rhs: 'good', + dir: 'rhs', + name: 'lhs has multiple deleted lines at end (issue #115)' + }, + { + lhs: 'good', + rhs: '\ngood', + dir: 'lhs', + name: 'rhs has added line at start' + }, + { + lhs: 'good', + rhs: '\na\nb\nc\ngood', + dir: 'lhs', + name: 'rhs has multiple added lines at start' + }, + { + lhs: 'good', + rhs: 'good\n', + dir: 'lhs', + name: 'rhs has added line at end' + }, + { + lhs: 'good', + rhs: 'good\na\nb\nc', + dir: 'lhs', + name: 'rhs has multiple added lines at end' + }]; + + opts.forEach((opt, i) => { + it(`merge-case-${i} should merge change to ${opt.dir} where ${opt.name}`, function (done) { + const editor = init({ + height: 100, + license: 'lgpl-separate-notice', + change_timeout: 0, + lhs: (setValue) => setValue(opt.lhs), + rhs: (setValue) => setValue(opt.rhs), + }); + const test = () => { + try { + expect(editor.get('lhs')).to.equal(opt.lhs); + expect(editor.get('rhs')).to.equal(opt.rhs); + expect(editor.el.querySelectorAll('.merge-button').length > 0) + .to.be.true; + done(); + } catch (ex) { + done(ex); + } + }; + editor.el.addEventListener('updated', test, { once: true }); + }); + }); + }); + + describe('options', () => { + it('should not change any options if object empty', 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 { + const newOptions = editor.options(); + expect(defaultOptions).to.deep.equal(newOptions); + done(); + } catch (ex) { + done(ex); + } + }; + editor.el.addEventListener('updated', test, { once: true }); + const defaultOptions = editor.options(); + editor.options({}); + }); + + it('should change options', 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 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({ + ...defaultOptions, + ...changes + }); + done(); + } catch (ex) { + done(ex); + } + }; + editor.el.addEventListener('updated', test, { once: true }); + const defaultOptions = editor.options(); + editor.options(changes); + }); + + it('should ignore white-space', function () { + const editor = init({ + height: 100, + change_timeout: 0, + license: 'lgpl-separate-notice', + ignorews: true, + lhs: (setValue) => setValue('\tthis is text'), + rhs: (setValue) => setValue('this\tis\ttext\t\t') + }); + expect(editor.diff()).to.equal(''); + }); + + it('should ignore case', function () { + const editor = init({ + height: 100, + change_timeout: 0, + license: 'lgpl-separate-notice', + ignorecase: true, + lhs: (setValue) => setValue('thIS IS text'), + rhs: (setValue) => setValue('this is text') + }); + expect(editor.diff()).to.equal(''); + }); + + it('should ignore accented characters', function () { + const editor = init({ + height: 100, + change_timeout: 0, + license: 'lgpl-separate-notice', + ignoreaccents: true, + lhs: (setValue) => setValue('comunicação'), + rhs: (setValue) => setValue('comunicacao') + }); + expect(editor.diff()).to.equal(''); + }); + + it('should ignore white-space, case, and accented characters', function () { + const editor = init({ + height: 100, + change_timeout: 0, + license: 'lgpl-separate-notice', + ignorews: true, + ignoreaccents: true, + ignorecase: true, + lhs: (setValue) => setValue('This is Comunicação'), + rhs: (setValue) => setValue('\t\tthis\tis\tcomunicacao') + }); + expect(editor.diff()).to.equal(''); + }); + }); + + describe('resize', () => { + it('should trigger resize', function (done) { + const loaded = simple.stub(); + const resized = simple.stub(); + const editor = init({ + height: 100, + change_timeout: 0, + license: 'lgpl-separate-notice', + loaded, + resized + }); + const test = () => { + try { + expect(loaded.calls).to.have.length(1); + expect(resized.calls).to.have.length(3); + done(); + } catch (ex) { + done(ex); + } + }; + editor.el.addEventListener('updated', test, { once: true }); + 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: (setValue) => setValue(macbeth), + rhs: (setValue) => setValue(macbeth.replace(/place/g, 'space')) + }); + const test = () => { + try { + const { + _is_change_in_view: isVisible, + _get_viewport_side: 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.el.addEventListener('updated', () => { + try { + vp = getViewport.call(editor._diffView, 'lhs'); + expect(vp).to.deep.equal({ from: 680, to: 708 }); + expect(isVisible('lhs', vp, change)).to.be.true; + expect(isVisible('rhs', vp, change)).to.be.true; + done(); + } catch (ex) { + done(ex); + } + }, { once: true }); + editor.scrollTo('lhs', 696); + } catch (ex) { + done(ex); + } + }; + expect(editor.diff()).to.include('696c696'); + editor.el.addEventListener('updated', test, { once: true }); + }); + }); + + describe('scrollToDiff', () => { + it('should scroll next to specific diff and scroll into view', function (done) { + const editor = init({ + height: 100, + change_timeout: 0, + license: 'lgpl-separate-notice', + lhs: (setValue) => setValue(macbeth), + rhs: (setValue) => setValue(macbeth.replace(/place/g, 'space')) + }); + let count = 0; + const test = () => { + if (count < 4) { + count += 1; + editor.scrollToDiff('next'); + return; + } + try { + const { + _is_change_in_view: isVisible, + _get_viewport_side: getViewport, + changes + } = editor._diffView; + const change = changes[4]; + + vp = getViewport.call(editor._diffView, 'lhs'); + expect(vp).to.deep.equal({ from: 666, to: 706 }); + expect(isVisible('lhs', vp, change)).to.be.true; + expect(isVisible('rhs', vp, change)).to.be.true; + done(); + } catch (ex) { + done(ex); + } finally { + editor.el.removeEventListener(test); + } + }; + expect(editor.diff()).to.include('696c696'); + editor.el.addEventListener('updated', test); + }); + }); + + describe('search', () => { + it('should search', function (done) { + const editor = init({ + height: 100, + change_timeout: 0, + license: 'lgpl-separate-notice', + lhs: (setValue) => setValue(macbeth), + rhs: (setValue) => setValue(macbeth.replace(/place/g, 'space')) + }); + let count = 0; + const test = () => { + if (count < 2) { + // init, scroll, search + count += 1; + if (count === 2) { + editor.search('lhs', 'knock'); + } + return; + } + try { + const { + _is_change_in_view: isVisible, + _get_viewport_side: getViewport, + changes + } = editor._diffView; + const change = changes[4]; + + const vp = getViewport.call(editor._diffView, 'lhs'); + expect(vp).to.deep.equal({ from: 323, to: 349 }); + done(); + } catch (ex) { + done(ex); + } finally { + editor.el.removeEventListener(test); + } + }; + expect(editor.diff()).to.include('696c696'); + editor.el.addEventListener('updated', test); + }); + }); + + describe('swap', () => { + it('should swap sides', (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('right-hand side text'); + expect(editor.get('rhs')).to.equal('left-hand side text'); + done(); + } catch (ex) { + done(ex); + } + }; + editor.el.addEventListener('updated', () => { + editor.swap(); + editor.el.addEventListener('updated', test, { once: true }); + }, { once: true }); + }); + }); + + describe('unmarkup', () => { + it('should remove markup', (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 { + const found = document.querySelector('.mergely.ch.d.lhs'); + expect(found).to.be.null; + done(); + } catch (ex) { + done(ex); + } + }; + editor.el.addEventListener('updated', () => { + try { + const found = document.querySelector('.mergely.ch.d.lhs'); + expect(found).to.not.be.null; + } catch (ex) { + return done(ex); + } + editor.el.addEventListener('updated', test, { once: true }); + editor.unmarkup(); + }, { once: true }); + }); + }); + + describe('update', () => { + it('should not trigger update on init when autoupdate is false', (done) => { + const editor = init({ + height: 100, + change_timeout: 0, + autoupdate: false, + license: 'lgpl-separate-notice', + lhs: (setValue) => setValue('left-hand side text'), + rhs: (setValue) => setValue('right-hand side text') + }); + const updated = simple.stub(); + editor.el.addEventListener('updated', updated, { once: true }); + setTimeout(() => { + const found = document.querySelector('.mergely.ch.d.lhs'); + try { + expect(found).to.be.null; + expect(updated.calls).to.have.length(0); + done(); + } catch (ex) { + done(ex); + } + }, 150); + }); + + it('should not trigger update on options when autoupdate is false', (done) => { + const editor = init({ + height: 100, + change_timeout: 0, + autoupdate: false, + license: 'lgpl-separate-notice', + lhs: (setValue) => setValue('left-hand side text'), + rhs: (setValue) => setValue('right-hand side text') + }); + const updated = simple.stub(); + editor.el.addEventListener('updated', updated, { once: true }); + setTimeout(() => { + const found = document.querySelector('.mergely.ch.d.lhs'); + try { + expect(found).to.be.null; + expect(updated.calls).to.have.length(0); + done(); + } catch (ex) { + done(ex); + } + }, 150); + editor.options({ wrap_lines: false }); + }); + + it('should not trigger update on lhs/rhs change when autoupdate is false', (done) => { + const editor = init({ + height: 100, + change_timeout: 0, + autoupdate: false, + license: 'lgpl-separate-notice', + lhs: (setValue) => setValue('left-hand side text'), + rhs: (setValue) => setValue('right-hand side text') + }); + const updated = simple.stub(); + editor.el.addEventListener('updated', updated, { once: true }); + setTimeout(() => { + const found = document.querySelector('.mergely.ch.d.lhs'); + try { + expect(found).to.be.null; + expect(updated.calls).to.have.length(0); + done(); + } catch (ex) { + done(ex); + } + }, 150); + editor.lhs('lhs text'); + editor.rhs('rhs text'); + }); + + it('should not trigger update on scroll when autoupdate is false', (done) => { + const editor = init({ + height: 100, + change_timeout: 0, + autoupdate: false, + license: 'lgpl-separate-notice', + lhs: (setValue) => setValue(macbeth), + rhs: (setValue) => setValue(macbeth.replace(/place/g, 'space')) + }); + const updated = simple.stub(); + editor.el.addEventListener('updated', updated, { once: true }); + setTimeout(() => { + const found = document.querySelector('.mergely.ch.d.lhs'); + try { + expect(found).to.be.null; + expect(updated.calls).to.have.length(0); + done(); + } catch (ex) { + done(ex); + } + }, 150); + editor.scrollTo('lhs', 696); + }); + + it('should manually update when autoupdate is false', (done) => { + const editor = init({ + height: 100, + change_timeout: 0, + autoupdate: false, + license: 'lgpl-separate-notice', + lhs: (setValue) => setValue(macbeth), + rhs: (setValue) => setValue(macbeth.replace(/place/g, 'space')) + }); + const test = () => { + try { + const found = document.querySelector('.mergely.ch.d.lhs'); + expect(found).not.to.be.null; + done(); + } catch (ex) { + done(ex); + } + }; + editor.el.addEventListener('updated', test, { once: true }); + setTimeout(() => { + editor.scrollTo('lhs', 696); + editor.update(); + }, 80); + }); + }); + + describe('_is_change_in_view', () => { + 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: (setValue) => setValue(macbeth), + rhs: (setValue) => setValue(macbeth) + }); + const test = () => { + try { + const { _is_change_in_view: 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.el.addEventListener('updated', test, { once: true }); + }); + + 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: (setValue) => setValue(macbeth), + rhs: (setValue) => setValue(macbeth) + }); + const test = () => { + try { + const { _is_change_in_view: 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.el.addEventListener('updated', test, { once: true }); + }); + + 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: (setValue) => setValue(macbeth), + rhs: (setValue) => setValue(macbeth) + }); + const test = () => { + try { + const { _is_change_in_view: 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.el.addEventListener('updated', test, { once: true }); + }); + + 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: (setValue) => setValue(macbeth), + rhs: (setValue) => setValue(macbeth) + }); + const test = () => { + try { + const { _is_change_in_view: 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.el.addEventListener('updated', test, { once: true }); + }); + + 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: (setValue) => setValue(macbeth), + rhs: (setValue) => setValue(macbeth) + }); + const test = () => { + try { + const { _is_change_in_view: 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.el.addEventListener('updated', test, { once: true }); + }); + + 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: (setValue) => setValue(macbeth), + rhs: (setValue) => setValue(macbeth) + }); + const test = () => { + try { + const { _is_change_in_view: 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.el.addEventListener('updated', test, { once: true }); + }); + }); + + 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: (setValue) => setValue(xss), + rhs: (setValue) => setValue('') + }); + // 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); + }); + }); +}); diff --git a/tests/mergely.spec.js b/tests/mergely.spec.js deleted file mode 100644 index 396a5e5..0000000 --- a/tests/mergely.spec.js +++ /dev/null @@ -1,714 +0,0 @@ -const jQuery = require('jquery'); -const CodeMirror = require('CodeMirror'); -const mergely = require('../src/mergely'); -const macbeth = require('./data/macbeth').join('\n'); -const $ = jQuery; - -(function($) { - $.fn.hasScrollBar = function() { - return this.get(0).scrollHeight - 2 > this.height(); - } -})(jQuery); - -const defaultOptions = { - autoupdate: true, - autoresize: true, - rhs_margin: 'right', - wrap_lines: false, - line_numbers: true, - lcs: true, - sidebar: true, - viewport: false, - ignorews: false, - ignorecase: false, - ignoreaccents: false, - fadein: 'fast', - resize_timeout: 500, - change_timeout: 150, - fgcolor: { - a: '#4ba3fa', - c: '#a3a3a3', - d: '#ff7f7f', - ca: '#4b73ff', - cc: '#434343', - cd: '#ff4f4f' - }, - bgcolor: '#eee', - vpcolor: 'rgba(0, 0, 200, 0.5)', - license: '', - width: 'auto', - height: 'auto', - cmsettings: { - styleSelectedText: true - } -}; - -describe('mergely', function () { - function init(options) { - $('body').css({'margin': '0px'}).append('
'); - const editor = $('#mergely'); - editor.mergely(options); - return editor; - }; - - afterEach(() => { - const mergely = $('#mergely'); - mergely.mergely('unbind'); - mergely.mergelyUnregister(); - mergely.remove(); - }); - - describe('initialization', () => { - it('initializes without arguments', function (done) { - $(document).ready(() => { - const editor = init({ - height: 100, - license: 'lgpl-separate-notice' - }); - editor.id = 1; - expect(editor).to.exist; - expect(editor.mergely).to.exist; - const children = editor.children(); - expect(children.length).to.equal(6); - expect($(children[0]).attr('id')).to.equal('mergely-splash'); - expect($(children[1]).attr('class')).to.equal('mergely-margin'); - expect($(children[2]).attr('class')).to.equal('mergely-column'); - expect($(children[3]).attr('class')).to.equal('mergely-canvas'); - expect($(children[4]).attr('class')).to.equal('mergely-column'); - expect($(children[5]).attr('class')).to.equal('mergely-margin'); - //expect($('body').hasScrollBar()).to.equal(false); - expect($('.mergely-editor-lhs .CodeMirror-code').text()).to.equal(''); - expect($('.mergely-editor-rhs .CodeMirror-code').text()).to.equal(''); - done(); - }); - }); - - it('initializes with static arguments for lhs/rhs text', function (done) { - $(document).ready(() => { - const editor = init({ - height: 100, - license: 'lgpl-separate-notice', - lhs: (setValue) => setValue('left-hand side text'), - rhs: (setValue) => setValue('right-hand side text') - }); - expect(editor).to.exist; - expect(editor.mergely).to.exist; - const children = editor.children(); - expect(children.length).to.equal(6); - expect($(children[0]).attr('id')).to.equal('mergely-splash'); - expect($(children[1]).attr('class')).to.equal('mergely-margin'); - expect($(children[2]).attr('class')).to.equal('mergely-column'); - expect($(children[3]).attr('class')).to.equal('mergely-canvas'); - expect($(children[4]).attr('class')).to.equal('mergely-column'); - expect($(children[5]).attr('class')).to.equal('mergely-margin'); - expect($('#mergely-editor-lhs .CodeMirror-code .CodeMirror-line').text()).to.equal('left-hand side text'); - expect($('#mergely-editor-rhs .CodeMirror-code .CodeMirror-line').text()).to.equal('right-hand side text'); - done(); - }); - }); - - it('initializes with no sidebar', function (done) { - $(document).ready(() => { - const editor = init({ - height: 100, - license: 'lgpl-separate-notice', - sidebar: false - }); - expect(editor).to.exist; - expect(editor.mergely).to.exist; - const children = editor.children(); - expect(children.length).to.equal(6); - expect($(children[0]).attr('id')).to.equal('mergely-splash'); - expect($(children[1]).attr('class')).to.equal('mergely-margin'); - expect($(children[1]).css('display')).to.equal('none'); - expect($(children[2]).attr('class')).to.equal('mergely-column'); - expect($(children[3]).attr('class')).to.equal('mergely-canvas'); - expect($(children[4]).attr('class')).to.equal('mergely-column'); - expect($(children[5]).attr('class')).to.equal('mergely-margin'); - expect($(children[5]).css('display')).to.equal('none'); - done(); - }); - }); - - it('initializes with default options', function (done) { - $(document).ready(() => { - const editor = init(); - const el = $('#mergely'); - const options = el.mergely('options'); - expect(options).to.deep.include(defaultOptions); - expect(options.lhs).to.be.a('function'); - expect(options.rhs).to.be.a('function'); - expect(options.loaded).to.be.a('function'); - expect(options.resize).to.be.a('function'); - expect(options.resized).to.be.a('function'); - done(); - }); - }); - - it('initializes with options', function (done) { - $(document).ready(() => { - const userOptions = { - autoupdate: false, - fgcolor: { - a: 'red', - c: 'green', - d: 'blue' - }, - cmsettings: { - lineSeparator: '\n', - readOnly: true - }, - rhs_cmsettings: { - readOnly: false - } - }; - const editor = init(userOptions); - const el = $('#mergely'); - const options = el.mergely('options'); - expect(options).to.deep.include({ - ...defaultOptions, - ...userOptions - }); - expect(options.lhs).to.be.a('function'); - expect(options.rhs).to.be.a('function'); - expect(options.loaded).to.be.a('function'); - expect(options.resize).to.be.a('function'); - expect(options.resized).to.be.a('function'); - done(); - }); - }); - }); - - describe('get', () => { - it('gets lhs and rhs text', function (done) { - $(document).ready(() => { - const editor = init({ - height: 100, - license: 'lgpl-separate-notice', - lhs: (setValue) => setValue('left-hand side text'), - rhs: (setValue) => setValue('right-hand side text') - }); - expect($('#mergely').mergely('get', 'lhs')).to.equal('left-hand side text'); - expect($('#mergely').mergely('get', 'rhs')).to.equal('right-hand side text'); - done(); - }); - }); - }); - - describe('mergeCurrentChange', () => { - const opts = [{ // add 0-7 merge lhs - lhs: '', - rhs: '\na', - dir: 'lhs', - name: 'add is line 2 and insert into end of empty lhs' - }, - { - lhs: '', - rhs: 'a\n', - dir: 'lhs', - name: 'add is line 1 and insert into end of empty lhs' - }, - { - lhs: 'a', - rhs: 'a\nb', - dir: 'lhs', - name: 'add is line 2 and insert into end to existing lhs' - }, - { - lhs: 'a', - rhs: 'a\nbcd', - dir: 'lhs', - name: 'add is line 2 of length 3 and insert into end of existing lhs' - }, - { - lhs: 'a', - rhs: 'a\nb\nc\nd', - dir: 'lhs', - name: 'add is lines 2-4 and insert into end of existing lhs' - }, - { - lhs: '', - rhs: '\nabcd', - dir: 'lhs', - name: 'add is line 2 of length 4 and lhs is empty' - }, - { - lhs: 'abcd', - rhs: '\nabcd', - dir: 'lhs', - name: 'add is line 1 cr and insert into front of lhs' - }, - { - lhs: 'a\nd', - rhs: 'a\nb\nc\nd', - dir: 'lhs', - name: 'add is lines 2-3 and insert into middle of lhs' - }, - // add 8-15 merge rhs - { - lhs: '', - rhs: '\na', - dir: 'rhs', - name: 'add is line 2 and empty lhs will replace non-empty rhs' - }, - { - lhs: '', - rhs: 'a\n', - dir: 'rhs', - name: 'add is line 1 and empty lhs will replace line 1 of rhs' - }, - { - lhs: 'a', - rhs: 'a\nb', - dir: 'rhs', - name: 'add is line 2 and empty end lhs will replace line 2 of rhs' - }, - { - lhs: 'a', - rhs: 'a\nbcd', - dir: 'rhs', - name: 'add is line 2 of length 3 and empty end lhs will replace line 2 of rhs' - }, - { - lhs: 'a', - rhs: 'a\nb\nc\nd', - dir: 'rhs', - name: 'add is lines 2-4 and empty end lhs will replace last 3 lines of rhs' - }, - { - lhs: '', - rhs: '\nabcd', - dir: 'rhs', - name: 'add is line 2 of length 4 and empty lhs will replace last line of rhs' - }, - { - lhs: 'abcd', - rhs: '\nabcd', - dir: 'rhs', - name: 'add is line 1 cr and empty front will replace first line of rhs' - }, - { - lhs: 'a\nd', - rhs: 'a\nb\nc\nd', - dir: 'rhs', - name: 'add is lines 2-3 and empty middle will replace middle two lines of rhs' - }, - // change 16-21 merge lhs - { - lhs: 'a', - rhs: '', - dir: 'lhs', - name: 'change is line 1 and will replace empty rhs' - }, - { - lhs: 'a\nb\nc', - rhs: '', - dir: 'lhs', - name: 'change is lines 1-3 and will replace empty rhs' - }, - { - lhs: 'a', - rhs: 'b', - dir: 'lhs', - name: 'change is line 1 and will replace opposite line 1' - }, - { - lhs: 'a\nb', - rhs: 'c\nd', - dir: 'lhs', - name: 'change is lines 2 and will replace opposite line 2' - }, - { - lhs: 'a\nbc\nef', - rhs: 'a\nbcd\nef', - dir: 'lhs', - name: 'change is middle line 2 and will replace opposite middle line 2' - }, - { - lhs: 'a\nb\n\n', - rhs: 'a1\nb1\nc1\nd', - dir: 'lhs', - name: 'change is all lines 1-3 and will replace opposite all lines 1-4' - }, - // change 22-27 merge rhs - { - lhs: 'a', - rhs: '', - dir: 'rhs', - name: 'change is line 1 and will empty the rhs' - }, - { - lhs: 'a\nb\nc', - rhs: '', - dir: 'rhs', - name: 'change is lines 1-3 and will replace empty rhs' - }, - { - lhs: 'a', - rhs: 'b', - dir: 'rhs', - name: 'change is line 1 and will replace opposite rhs line 1' - }, - { - lhs: 'a\nb', - rhs: 'c\nd', - dir: 'rhs', - name: 'change is lines 2 and will replace opposite rhs line 2' - }, - { - lhs: 'a\nbc\nef', - rhs: 'a\nbcd\nef', - dir: 'rhs', - name: 'change is middle line 2 and will replace opposite rhs middle line 2' - }, - { - lhs: 'a\nb\n\n', - rhs: 'a1\nb1\nc1\nd', - dir: 'rhs', - name: 'change is all lines 1-3 and will replace opposite rhs all lines 1-4' - }, - // delete 28-35 merge lhs - { - lhs: '\na', - rhs: '', - dir: 'lhs', - name: 'delete is line 2 and will merge empty rhs' - }, - { - lhs: 'a\n', - rhs: '', - dir: 'lhs', - name: 'delete is line 1 and will merge empty rhs leaving cr on line 2' - }, - { - lhs: 'a\nb', - rhs: 'a', - dir: 'lhs', - name: 'delete is line 2 and will merge into end to existing rhs' - }, - { - lhs: 'a\nbcd', - rhs: 'a', - dir: 'lhs', - name: 'delete is line 2 of length 3 and insert into end of existing rhs' - }, - { - lhs: 'a\nb\nc\nd', - rhs: 'a', - dir: 'lhs', - name: 'delete is lines 2-4 and insert into end of existing rhs' - }, - { - lhs: '\nabcd', - rhs: '', - dir: 'lhs', - name: 'delete is line 2 of length 4 and insert into end of empty rhs' - }, - { - lhs: '\nabcd', - rhs: 'abcd', - dir: 'lhs', - name: 'delete is line 1 cr and insert into front of rhs' - }, - { - lhs: 'a\nb\nc\nd', - rhs: 'a\nd', - dir: 'lhs', - name: 'delete is lines 2-3 and insert into middle of rhs' - }, - { - lhs: 'a\nb\nc', - rhs: 'a\nb\nx\nc', - dir: 'lhs', - name: 'delete line 3 of length 1 of rhs and will be replaced with empty line 2 of lhs' - }, - // delete 36-43 merge rhs - { - lhs: '\na', - rhs: '', - dir: 'rhs', - name: 'delete is line 2 and will merge empty rhs' - }, - { - lhs: 'a\n', - rhs: '', - dir: 'rhs', - name: 'delete is line 1 and will merge empty rhs leaving cr on line 2' - }, - { - lhs: 'a\nb', - rhs: 'a', - dir: 'rhs', - name: 'delete is line 2 and will merge into end to existing rhs' - }, - { - lhs: 'a\nbcd', - rhs: 'a', - dir: 'rhs', - name: 'delete is line 2 of length 3 and insert into end of existing rhs' - }, - { - lhs: 'a\nb\nc\nd', - rhs: 'a', - dir: 'rhs', - name: 'delete is lines 2-4 and insert into end of existing rhs' - }, - { - lhs: '\nabcd', - rhs: '', - dir: 'rhs', - name: 'delete is line 2 of length 4 and insert into end of empty rhs' - }, - { - lhs: '\nabcd', - rhs: 'abcd', - dir: 'rhs', - name: 'delete is line 1 cr and insert into front of rhs' - }, - { - lhs: 'a\nb\nc\nd', - rhs: 'a\nd', - dir: 'rhs', - name: 'delete is lines 2-3 and insert into middle of rhs' - }, - { - lhs: 'a\nb\nc', - rhs: 'a\nb\nx\nc', - dir: 'rhs', - name: 'merge rhs line 3 of length 1 and insert after line 2 of lhs' - }, - { - lhs: '\ngood', - rhs: 'good', - dir: 'rhs', - name: 'lhs has deleted line at start' - }, - { - lhs: '\na\nb\nc\ngood', - rhs: 'good', - dir: 'rhs', - name: 'lhs has multiple deleted lines at start' - }, - { - lhs: 'good\n', - rhs: 'good', - dir: 'rhs', - name: 'lhs has deleted line at end (issue #115)' - }, - { - lhs: 'good\na\nb\nc', - rhs: 'good', - dir: 'rhs', - name: 'lhs has multiple deleted lines at end (issue #115)' - }, - { - lhs: 'good', - rhs: '\ngood', - dir: 'lhs', - name: 'rhs has added line at start' - }, - { - lhs: 'good', - rhs: '\na\nb\nc\ngood', - dir: 'lhs', - name: 'rhs has multiple added lines at start' - }, - { - lhs: 'good', - rhs: 'good\n', - dir: 'lhs', - name: 'rhs has added line at end' - }, - { - lhs: 'good', - rhs: 'good\na\nb\nc', - dir: 'lhs', - name: 'rhs has multiple added lines at end' - }]; - - opts.forEach((opt, i) => { - it(`merge-case-${i} should merge change to ${opt.dir} where ${opt.name}`, function (done) { - $(document).ready(() => { - const editor = init({ - height: 100, - license: 'lgpl-separate-notice', - change_timeout: 0, - lhs: (setValue) => setValue(opt.lhs), - rhs: (setValue) => setValue(opt.rhs) - }); - expect($('#mergely').mergely('get', 'lhs')).to.equal(opt.lhs); - expect($('#mergely').mergely('get', 'rhs')).to.equal(opt.rhs); - setTimeout(() => { - for (let i = 0; i < opt.next; ++i) { - $('#mergely').mergely('scrollToDiff', 'next'); - } - expect($('.merge-button').length > 0).to.be.true; - $('#mergely').mergely('mergeCurrentChange', opt.dir); - if (opt.dir === 'lhs') { - expect($('#mergely').mergely('get', 'lhs')).to.equal(opt.expect || opt.rhs); - expect($('#mergely').mergely('get', 'rhs')).to.equal(opt.rhs); - } else { - expect($('#mergely').mergely('get', 'lhs')).to.equal(opt.lhs); - expect($('#mergely').mergely('get', 'rhs')).to.equal(opt.expect || opt.lhs); - } - done(); - }, 10); - }); - }); - }); - }); - - describe('_is_change_in_view', () => { - it('should be false when change less-than viewport', function (done) { - $(document).ready(() => { - const editor = init({ - height: 100, - viewport: true, - license: 'lgpl-separate-notice', - lhs: (setValue) => setValue(macbeth), - rhs: (setValue) => setValue(macbeth) - }); - const { mergely } = $('#mergely'); - expect($('#mergely').mergely('_is_change_in_view', 'lhs', {from: 10, to: 20}, { - 'lhs-line-from': 0, - 'lhs-line-to': 9 - })).to.be.false; - done(); - }); - }); - - it('should be true when change less-than-equal viewport', function (done) { - $(document).ready(() => { - const editor = init({ - height: 100, - viewport: true, - license: 'lgpl-separate-notice', - lhs: (setValue) => setValue(macbeth), - rhs: (setValue) => setValue(macbeth) - }); - const { mergely } = $('#mergely'); - expect($('#mergely').mergely('_is_change_in_view', 'lhs', {from: 10, to: 20}, { - 'lhs-line-from': 0, - 'lhs-line-to': 10 - })).to.be.true; - done(); - }); - }); - - it('should be false when change greater-than viewport', function (done) { - $(document).ready(() => { - const editor = init({ - height: 100, - viewport: true, - license: 'lgpl-separate-notice', - lhs: (setValue) => setValue(macbeth), - rhs: (setValue) => setValue(macbeth) - }); - const { mergely } = $('#mergely'); - expect($('#mergely').mergely('_is_change_in_view', 'lhs', {from: 10, to: 20}, { - 'lhs-line-from': 21, - 'lhs-line-to': 22 - })).to.be.false; - done(); - }); - }); - - it('should be true when change straddles viewport from', function (done) { - $(document).ready(() => { - const editor = init({ - height: 100, - viewport: true, - license: 'lgpl-separate-notice', - lhs: (setValue) => setValue(macbeth), - rhs: (setValue) => setValue(macbeth) - }); - const { mergely } = $('#mergely'); - expect($('#mergely').mergely('_is_change_in_view', 'lhs', {from: 10, to: 20}, { - 'lhs-line-from': 5, - 'lhs-line-to': 11 - })).to.be.true; - done(); - }); - }); - - it('should be true when change straddles viewport to', function (done) { - $(document).ready(() => { - const editor = init({ - height: 100, - viewport: true, - license: 'lgpl-separate-notice', - lhs: (setValue) => setValue(macbeth), - rhs: (setValue) => setValue(macbeth) - }); - const { mergely } = $('#mergely'); - expect($('#mergely').mergely('_is_change_in_view', 'lhs', {from: 10, to: 20}, { - 'lhs-line-from': 15, - 'lhs-line-to': 21 - })).to.be.true; - done(); - }); - }); - - it('should be true when change encompasses viewport', function (done) { - $(document).ready(() => { - const editor = init({ - height: 100, - viewport: true, - license: 'lgpl-separate-notice', - lhs: (setValue) => setValue(macbeth), - rhs: (setValue) => setValue(macbeth) - }); - const { mergely } = $('#mergely'); - expect($('#mergely').mergely('_is_change_in_view', 'lhs', {from: 10, to: 20}, { - 'lhs-line-from': 0, - 'lhs-line-to': 25 - })).to.be.true; - done(); - }); - }); - }); - - it('should not be vulnerable to XSS', function (done) { - function initXSS(options) { - $('body').get(0).innerHTML = "
"; - const divs = document.getElementsByTagName('div'); - editor = $(divs[0]); - editor.mergely(options); - return editor; - }; - - $(document).ready(() => { - const editor = initXSS({ - height: 100, - viewport: true, - license: 'lgpl-separate-notice', - lhs: (setValue) => setValue(macbeth), - rhs: (setValue) => setValue(macbeth) - }); - expect($('body').find('#injected')).to.have.length(0, 'expected no div with id injected'); - const divs = document.getElementsByTagName('div'); - expect(divs).to.have.length(1); - expect(divs[0].id).to.equal('mergely'); - done(); - }); - }); - - it('should ignore accented characters', function (done) { - $(document).ready(() => { - const editor = init({ - height: 100, - license: 'lgpl-separate-notice', - ignoreaccents: true, - lhs: (setValue) => setValue('comunicação'), - rhs: (setValue) => setValue('comunicacao') - }); - const { mergely } = $('#mergely'); - - $('#mergely').on('updated', () => { - const diff = $('#mergely').mergely('diff');; - expect(diff).to.equal(''); - done(); - }); - }); - }); -}); diff --git a/webpack.dev.js b/webpack.dev.js index 27371eb..c9f2686 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -29,8 +29,7 @@ module.exports = { resolve: { extensions: ['.js'], alias: { - 'CodeMirror': path.join(__dirname, 'node_modules', 'codemirror'), - 'jQuery': path.join(__dirname, 'node_modules', 'jquery') + 'CodeMirror': path.join(__dirname, 'node_modules', 'codemirror') } }, @@ -38,10 +37,6 @@ module.exports = { new HtmlWebpackPlugin({ template: path.join(__dirname, 'examples', 'app.html') }), - new webpack.ProvidePlugin({ - $: 'jquery', - jQuery: 'jquery' - }), new webpack.ProvidePlugin({ CodeMirror: 'codemirror' }) diff --git a/webpack.prod.js b/webpack.prod.js index 8fdae6b..a0bfa5f 100644 --- a/webpack.prod.js +++ b/webpack.prod.js @@ -25,7 +25,6 @@ module.exports = { extensions: ['.js'] }, externals: { - jquery: 'jQuery', CodeMirror: 'CodeMirror' }, plugins: [