diff --git a/CHANGELOG.md b/CHANGELOG.md index 1293b578d..fc6688dc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# 1.7.0 +* FIX: `getLineHTMLForExport()` no longer produces multiple copies of a line. **WARNING**: this could potentially break some plugins +* FIX: authorship of bullet points no longer changes when a second author edits them +* FIX: improved Firefox compatibility (non printable keys) +* FIX: `getPadPlainText()` was not working +* REQUIREMENTS: minimum required Node version is 6.9.0 LTS. The next release will require at least Node 8.9.0 LTS +* SECURITY: updated MySQL, Elasticsearch and PostgreSQL drivers +* SECURITY: started updating deprecated code and packages +* DOCS: documented --credentials, --apikey, --sessionkey. Better detailed contributors guidelines. Added a section on securing the installation + # 1.6.6 * FIX: line numbers are aligned with text again (broken in 1.6.4) * FIX: text entered between connection loss and reconnection was not saved @@ -490,7 +500,7 @@ * Plugin-specific settings in settings.json (finally allowing for things like a google analytics plugin) * Serve admin dashboard at /admin (still very limited, though) * Modify your settings.json through the newly created UI at /admin/settings - * Fix: Import
    's as
      's and not as ')) - pieces.push('
    1. ', lineContent || '
      '); - } - lists = lists.slice(0,whichList+1) - } else { - pieces.push('
    2. ', lineContent || '
      '); - } - } - } - else//outside any list, need to close line.listLevel of lists - { - if(lists.length > 0){ - if(lists[lists.length - 1][1] == "number"){ - pieces.push('
    '); - pieces.push(new Array(listLevels[listLevels.length - 2]).join('
')) - } else { - pieces.push(''); - pieces.push(new Array(listLevels[listLevels.length - 2]).join('')) - } - } - lists = [] - - var context = { + context = { line: line, lineContent: lineContent, apool: apool, attribLine: attribLines[i], text: textLines[i], padId: pad.id - } - - var lineContentFromHook = hooks.callAllStr("getLineHTMLForExport", context, " ", " ", ""); - - if (lineContentFromHook) + }; + var prevLine = null; + var nextLine = null; + if (i > 0) { - pieces.push(lineContentFromHook, ''); + prevLine = _analyzeLine(textLines[i -1], attribLines[i -1], apool); } - else + if (i < textLines.length) { - pieces.push(lineContent, '
'); + nextLine = _analyzeLine(textLines[i + 1], attribLines[i + 1], apool); + } + hooks.aCallAll('getLineHTMLForExport', context); + //To create list parent elements + if ((!prevLine || prevLine.listLevel !== line.listLevel) || (prevLine && line.listTypeName !== prevLine.listTypeName)) + { + var exists = _.find(openLists, function (item) + { + return (item.level === line.listLevel && item.type === line.listTypeName); + }); + if (!exists) { + var prevLevel = 0; + if (prevLine && prevLine.listLevel) { + prevLevel = prevLine.listLevel; + } + if (prevLine && line.listTypeName !== prevLine.listTypeName) + { + prevLevel = 0; + } + + for (var diff = prevLevel; diff < line.listLevel; diff++) { + openLists.push({level: diff, type: line.listTypeName}); + var prevPiece = pieces[pieces.length - 1]; + + if (prevPiece.indexOf("") === 0) + { + pieces.push("
  • "); + } + + if (line.listTypeName === "number") + { + pieces.push("
      "); + } + else + { + pieces.push("
        "); + } + } + } + } + + pieces.push("
      • ", context.lineContent); + + // To close list elements + if (nextLine && nextLine.listLevel === line.listLevel && line.listTypeName === nextLine.listTypeName) + { + pieces.push("
      • "); + } + if ((!nextLine || !nextLine.listLevel || nextLine.listLevel < line.listLevel) || (nextLine && line.listTypeName !== nextLine.listTypeName)) + { + var nextLevel = 0; + if (nextLine && nextLine.listLevel) { + nextLevel = nextLine.listLevel; + } + if (nextLine && line.listTypeName !== nextLine.listTypeName) + { + nextLevel = 0; + } + + for (var diff = nextLevel; diff < line.listLevel; diff++) + { + openLists = openLists.filter(function(el) + { + return el.level !== diff && el.type !== line.listTypeName; + }); + + if (pieces[pieces.length - 1].indexOf(""); + } + + if (line.listTypeName === "number") + { + pieces.push("
    "); + } + else + { + pieces.push(""); + } + } } } - } + else//outside any list, need to close line.listLevel of lists + { + context = { + line: line, + lineContent: lineContent, + apool: apool, + attribLine: attribLines[i], + text: textLines[i], + padId: pad.id + }; - for (var k = lists.length - 1; k >= 0; k--) - { - if(lists[k][1] == "number") - { - pieces.push('
  • '); + hooks.aCallAll("getLineHTMLForExport", context); + pieces.push(context.lineContent, "
    "); + } } - else - { - pieces.push(''); - } - } return pieces.join(''); } diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index a56e347db..4596f404c 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -264,7 +264,8 @@ function getAceFile(callback) { async.forEach(founds, function (item, callback) { var filename = item.match(/"([^"]*)"/)[1]; - var baseURI = 'http://localhost:' + settings.port; + // Hostname "invalid.invalid" is a dummy value to allow parsing as a URI. + var baseURI = 'http://invalid.invalid'; var resourceURI = baseURI + path.normalize(path.join('/static/', filename)); resourceURI = resourceURI.replace(/\\/g, '/'); // Windows (safe generally?) diff --git a/src/node/utils/NodeVersion.js b/src/node/utils/NodeVersion.js new file mode 100644 index 000000000..49c1efe85 --- /dev/null +++ b/src/node/utils/NodeVersion.js @@ -0,0 +1,54 @@ +/** + * Checks related to Node runtime version + */ + +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Quits if Etherpad is not running on a given minimum Node version + * + * @param {String} minNodeVersion Minimum required Node version + * @param {Function} callback Standard callback function + */ +exports.enforceMinNodeVersion = function(minNodeVersion, callback) { + const semver = require('semver'); + const currentNodeVersion = process.version; + + // we cannot use template literals, since we still do not know if we are + // running under Node >= 4.0 + if (semver.lt(currentNodeVersion, minNodeVersion)) { + console.error('Running Etherpad on Node ' + currentNodeVersion + ' is not supported. Please upgrade at least to Node ' + minNodeVersion); + } else { + console.debug('Running on Node ' + currentNodeVersion + ' (minimum required Node version: ' + minNodeVersion + ')'); + callback(); + } +}; + +/** + * Prints a warning if running on a supported but deprecated Node version + * + * @param {String} lowestNonDeprecatedNodeVersion all Node version less than this one are deprecated + * @param {Function} epRemovalVersion Etherpad version that will remove support for deprecated Node releases + */ +exports.checkDeprecationStatus = function(lowestNonDeprecatedNodeVersion, epRemovalVersion, callback) { + const semver = require('semver'); + const currentNodeVersion = process.version; + + if (semver.lt(currentNodeVersion, lowestNonDeprecatedNodeVersion)) { + console.warn(`Support for Node ${currentNodeVersion} will be removed in Etherpad ${epRemovalVersion}. Please consider updating at least to Node ${lowestNonDeprecatedNodeVersion}`); + } + + callback(); +}; diff --git a/src/node/utils/caching_middleware.js b/src/node/utils/caching_middleware.js index 65fe5d2f1..c01a95fb3 100644 --- a/src/node/utils/caching_middleware.js +++ b/src/node/utils/caching_middleware.js @@ -49,7 +49,7 @@ CachingMiddleware.prototype = new function () { (req.get('Accept-Encoding') || '').indexOf('gzip') != -1; var path = require('url').parse(req.url).path; - var cacheKey = (new Buffer(path)).toString('base64').replace(/[\/\+=]/g, ''); + var cacheKey = Buffer.from(path).toString('base64').replace(/[/+=]/g, ''); fs.stat(CACHE_DIR + 'minified_' + cacheKey, function (error, stats) { var modifiedSince = (req.headers['if-modified-since'] diff --git a/src/package.json b/src/package.json index cb243ccb1..fcf5b97c0 100644 --- a/src/package.json +++ b/src/package.json @@ -1,60 +1,84 @@ { - "name" : "ep_etherpad-lite", - "description" : "A Etherpad based on node.js", - "homepage" : "http://etherpad.org", - "keywords" : ["etherpad", "realtime", "collaborative", "editor"], - "author" : "Etherpad Foundation", - "contributors" : [ - { "name": "John McLear" }, - { "name": "Hans Pinckaers" }, - { "name": "Robin Buse" }, - { "name": "Marcel Klehr" }, - { "name": "Peter Martischka" } - ], - "dependencies" : { - "etherpad-yajsml" : "0.0.2", - "request" : "2.83.0", - "etherpad-require-kernel" : "1.0.9", - "resolve" : "1.1.7", - "socket.io" : "1.7.3", - "ueberdb2" : "0.3.8", - "express" : "4.13.4", - "express-session" : "1.13.0", - "cookie-parser" : "1.3.4", - "async" : "0.9.0", - "clean-css" : "3.4.19", - "uglify-js" : "2.6.2", - "formidable" : "1.2.1", - "log4js" : "0.6.35", - "cheerio" : "0.20.0", - "async-stacktrace" : "0.0.2", - "npm" : ">=4.0.2", - "ejs" : "2.5.7", - "graceful-fs" : "4.1.3", - "slide" : "1.1.6", - "semver" : "5.1.0", - "security" : "1.0.0", - "tinycon" : "0.0.1", - "underscore" : "1.8.3", - "unorm" : "1.4.1", - "languages4translatewiki" : "0.1.3", - "swagger-node-express" : "2.1.3", - "channels" : "0.0.4", - "jsonminify" : "0.4.1", - "measured" : "1.1.0", - "mocha" : "5.0.5", - "supertest" : "3.0.0" + "name": "ep_etherpad-lite", + "description": "A Etherpad based on node.js", + "homepage": "http://etherpad.org", + "keywords": [ + "etherpad", + "realtime", + "collaborative", + "editor" + ], + "author": "Etherpad Foundation", + "contributors": [ + { + "name": "John McLear" + }, + { + "name": "Hans Pinckaers" + }, + { + "name": "Robin Buse" + }, + { + "name": "Marcel Klehr" + }, + { + "name": "Peter Martischka" + } + ], + "dependencies": { + "async": "0.9.0", + "async-stacktrace": "0.0.2", + "channels": "0.0.4", + "cheerio": "0.20.0", + "clean-css": "3.4.19", + "cookie-parser": "1.3.4", + "ejs": "2.5.7", + "etherpad-require-kernel": "1.0.9", + "etherpad-yajsml": "0.0.2", + "express": "4.16.3", + "express-session": "1.15.6", + "formidable": "1.2.1", + "graceful-fs": "4.1.3", + "jsonminify": "0.4.1", + "languages4translatewiki": "0.1.3", + "log4js": "0.6.35", + "measured-core": "1.11.2", + "npm": "6.4.0", + "object.values": "^1.0.4", + "request": "2.83.0", + "resolve": "1.1.7", + "security": "1.0.0", + "semver": "5.1.0", + "slide": "1.1.6", + "socket.io": "1.7.3", + "swagger-node-express": "2.1.3", + "tinycon": "0.0.1", + "ueberdb2": "0.4.0", + "uglify-js": "2.6.2", + "underscore": "1.8.3", + "unorm": "1.4.1" + }, + "bin": { + "etherpad-lite": "./node/server.js" }, - "bin": { "etherpad-lite": "./node/server.js" }, "devDependencies": { - "wd" : "1.6.1" - }, - "engines" : { "node" : ">=0.10.0", - "npm" : ">=1.0" - }, - "repository" : { "type" : "git", - "url" : "http://github.com/ether/etherpad-lite.git" - }, - "version" : "1.6.6", - "license" : "Apache-2.0" + "mocha": "5.2.0", + "nyc": "^12.0.2", + "supertest": "3.0.0", + "wd": "1.10.3" + }, + "engines": { + "node": ">=6.9.0", + "npm": ">=3.10.8" + }, + "repository": { + "type": "git", + "url": "http://github.com/ether/etherpad-lite.git" + }, + "scripts": { + "test": "nyc mocha --timeout 5000 ../tests/backend/specs/api" + }, + "version": "1.7.0", + "license": "Apache-2.0" } diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.js index 53b233e07..17b216624 100644 --- a/src/static/js/AttributeManager.js +++ b/src/static/js/AttributeManager.js @@ -4,6 +4,10 @@ var _ = require('./underscore'); var lineMarkerAttribute = 'lmkr'; +// Some of these attributes are kept for compatibility purposes. +// Not sure if we need all of them +var DEFAULT_LINE_ATTRIBUTES = ['author', 'lmkr', 'insertorder', 'start']; + // If one of these attributes are set to the first character of a // line it is considered as a line attribute marker i.e. attributes // set on this marker are applied to the whole line. @@ -35,6 +39,7 @@ var AttributeManager = function(rep, applyChangesetCallback) // it will be considered as a line marker }; +AttributeManager.DEFAULT_LINE_ATTRIBUTES = DEFAULT_LINE_ATTRIBUTES; AttributeManager.lineAttributes = lineAttributes; AttributeManager.prototype = _(AttributeManager.prototype).extend({ @@ -375,7 +380,7 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]); var countAttribsWithMarker = _.chain(attribs).filter(function(a){return !!a[1];}) - .map(function(a){return a[0];}).difference(['author', 'lmkr', 'insertorder', 'start']).size().value(); + .map(function(a){return a[0];}).difference(DEFAULT_LINE_ATTRIBUTES).size().value(); //if we have marker and any of attributes don't need to have marker. we need delete it if(hasMarker && !countAttribsWithMarker){ diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 90cefa506..8b0e2c3e9 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -1778,19 +1778,15 @@ function Ace2Inner(){ strikethrough: true, list: true }; - var OTHER_INCORPED_ATTRIBS = { - insertorder: true, - author: true - }; function isStyleAttribute(aname) { return !!STYLE_ATTRIBS[aname]; } - function isOtherIncorpedAttribute(aname) + function isDefaultLineAttribute(aname) { - return !!OTHER_INCORPED_ATTRIBS[aname]; + return AttributeManager.DEFAULT_LINE_ATTRIBUTES.indexOf(aname) !== -1; } function insertDomLines(nodeToAddAfter, infoStructs, isTimeUp) @@ -2757,9 +2753,12 @@ function Ace2Inner(){ function analyzeChange(oldText, newText, oldAttribs, newAttribs, optSelStartHint, optSelEndHint) { + // we need to take into account both the styles attributes & attributes defined by + // the plugins, so basically we can ignore only the default line attribs used by + // Etherpad function incorpedAttribFilter(anum) { - return !isOtherIncorpedAttribute(rep.apool.getAttribKey(anum)); + return !isDefaultLineAttribute(rep.apool.getAttribKey(anum)); } function attribRuns(attribs) @@ -3708,8 +3707,8 @@ function Ace2Inner(){ return; // This stops double enters in Opera but double Tabs still show on single tab keypress, adding keyCode == 9 to this doesn't help as the event is fired twice } var specialHandled = false; - var isTypeForSpecialKey = ((browser.msie || browser.safari || browser.chrome) ? (type == "keydown") : (type == "keypress")); - var isTypeForCmdKey = ((browser.msie || browser.safari || browser.chrome) ? (type == "keydown") : (type == "keypress")); + var isTypeForSpecialKey = ((browser.msie || browser.safari || browser.chrome || browser.firefox) ? (type == "keydown") : (type == "keypress")); + var isTypeForCmdKey = ((browser.msie || browser.safari || browser.chrome || browser.firefox) ? (type == "keydown") : (type == "keypress")); var stopped = false; inCallStackIfNecessary("handleKeyEvent", function() diff --git a/src/static/js/domline.js b/src/static/js/domline.js index 447c1497a..a7501fcc6 100644 --- a/src/static/js/domline.js +++ b/src/static/js/domline.js @@ -214,7 +214,7 @@ domline.createDomLine = function(nonEmpty, doesWrap, optBrowser, optDocument) result.clearSpans = function() { html = []; - lineClass = ''; // non-null to cause update + lineClass = 'ace-line'; result.lineMarker = 0; }; diff --git a/src/templates/admin/plugins-info.html b/src/templates/admin/plugins-info.html index 8dd0bf88e..8c259ac20 100644 --- a/src/templates/admin/plugins-info.html +++ b/src/templates/admin/plugins-info.html @@ -27,7 +27,7 @@

    Git sha: <%= gitCommit %>

    Installed plugins

    -
    <%- plugins.formatPlugins().replace(", ","\n") %>
    +
    <%- plugins.formatPlugins().replace(/, /g,"\n") %>

    Installed parts

    <%= plugins.formatParts() %>
    diff --git a/src/templates/export_html.html b/src/templates/export_html.html index b8893b717..5c017c8c1 100644 --- a/src/templates/export_html.html +++ b/src/templates/export_html.html @@ -20,7 +20,7 @@ ol { padding-left: 0; } body > ol { - counter-reset: first second third fourth fifth sixth seventh eigth ninth tenth eleventh twelth thirteenth fourteenth fifteenth sixteenth; + counter-reset: first second third fourth fifth sixth seventh eighth ninth tenth eleventh twelfth thirteenth fourteenth fifteenth sixteenth; } ol > li:before { content: counter(first) ". "; @@ -51,40 +51,40 @@ ol > ol > ol > ol > ol > ol > ol > li:before { counter-increment: seventh; } ol > ol > ol > ol > ol > ol > ol > ol > li:before { - content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eigth) ". "; - counter-increment: eigth; + content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eighth) ". "; + counter-increment: eighth; } ol > ol > ol > ol > ol > ol > ol > ol > ol > li:before { - content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eigth) "." counter(ninth) ". "; + content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eighth) "." counter(ninth) ". "; counter-increment: ninth; } ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > li:before { - content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eigth) "." counter(ninth) "." counter(tenth) ". "; + content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eighth) "." counter(ninth) "." counter(tenth) ". "; counter-increment: tenth; } ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > li:before { - content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eigth) "." counter(ninth) "." counter(tenth) "." counter(eleventh) ". "; + content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eighth) "." counter(ninth) "." counter(tenth) "." counter(eleventh) ". "; counter-increment: eleventh; } ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > li:before { - content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eigth) "." counter(ninth) "." counter(tenth) "." counter(eleventh) "." counter(twelth) ". "; - counter-increment: twelth; + content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eighth) "." counter(ninth) "." counter(tenth) "." counter(eleventh) "." counter(twelfth) ". "; + counter-increment: twelfth; } ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > li:before { - content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eigth) "." counter(ninth) "." counter(tenth) "." counter(eleventh) "." counter(twelth) "." counter(thirteenth) ". "; + content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eighth) "." counter(ninth) "." counter(tenth) "." counter(eleventh) "." counter(twelfth) "." counter(thirteenth) ". "; counter-increment: thirteenth; } ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > li:before { - content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eigth) "." counter(ninth) "." counter(tenth) "." counter(eleventh) "." counter(twelth) "." counter(thirteenth) "." counter(fourteenth) ". "; + content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eighth) "." counter(ninth) "." counter(tenth) "." counter(eleventh) "." counter(twelfth) "." counter(thirteenth) "." counter(fourteenth) ". "; counter-increment: fourteenth; } ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > li:before { - content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eigth) "." counter(ninth) "." counter(tenth) "." counter(eleventh) "." counter(twelth) "." counter(thirteenth) "." counter(fourteenth) "." counter(fifteenth) ". "; + content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eighth) "." counter(ninth) "." counter(tenth) "." counter(eleventh) "." counter(twelfth) "." counter(thirteenth) "." counter(fourteenth) "." counter(fifteenth) ". "; counter-increment: fifteenth; } ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > ol > li:before { - content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eigth) "." counter(ninth) "." counter(tenth) "." counter(eleventh) "." counter(twelth) "." counter(thirteenth) "." counter(fourteenth) "." counter(fifteenth) "." counter(sixthteenth) ". "; - counter-increment: sixthteenth; + content: counter(first) "." counter(second) "." counter(third) "." counter(fourth) "." counter(fifth) "." counter(sixth) "." counter(seventh) "." counter(eighth) "." counter(ninth) "." counter(tenth) "." counter(eleventh) "." counter(twelfth) "." counter(thirteenth) "." counter(fourteenth) "." counter(fifteenth) "." counter(sixteenth) ". "; + counter-increment: sixteenth; } ol { text-indent: 0px; diff --git a/tests/README.md b/tests/README.md index 201ee4c8c..6ab5dd23b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,9 +1,11 @@ # About this folder: Tests +Before running the tests, start an Etherpad instance on your machine. + ## Frontend -To run the tests, point your browser to `/tests/frontend` +To run the frontend tests, point your browser to `/tests/frontend` ## Backend -To run the tests, run ``bin/backendTests.sh`` +To run the backend tests, run `cd src` and then `npm test` diff --git a/tests/backend/specs/api/pad.js b/tests/backend/specs/api/pad.js index 1db90580e..26abfd2c8 100644 --- a/tests/backend/specs/api/pad.js +++ b/tests/backend/specs/api/pad.js @@ -14,7 +14,19 @@ var apiVersion = 1; var testPadId = makeid(); var lastEdited = ""; var text = generateLongText(); -var ULhtml = '
    • one
    • 2

      • UL2
    '; + +/* + * Html document with nested lists of different types, to test its import and + * verify it is exported back correctly + */ +var ulHtml = '
    • one
    • two
    • 0
    • 1
    • 2
      • 3
      • 4
    1. item
      1. item1
      2. item2
    '; + +/* + * When exported back, Etherpad produces an html which is not exactly the same + * textually, but at least it remains standard compliant and has an equal DOM + * structure. + */ +var expectedHtml = '
    • one
    • two
    • 0
    • 1
    • 2
      • 3
      • 4
    1. item
      1. item1
      2. item2
    '; describe('Connectivity', function(){ it('errors if can not connect', function(done) { @@ -522,8 +534,8 @@ describe('setHTML', function(){ }) describe('setHTML', function(){ - it('Sets the HTML of a Pad with a bunch of weird unordered lists inserted', function(done) { - api.get(endPoint('setHTML')+"&padID="+testPadId+"&html="+ULhtml) + it('Sets the HTML of a Pad with complex nested lists of different types', function(done) { + api.get(endPoint('setHTML')+"&padID="+testPadId+"&html="+ulHtml) .expect(function(res){ if(res.body.code !== 0) throw new Error("List HTML cant be imported") }) @@ -533,12 +545,22 @@ describe('setHTML', function(){ }) describe('getHTML', function(){ - it('Gets the HTML of a Pad with a bunch of weird unordered lists inserted', function(done) { + it('Gets back the HTML of a Pad with complex nested lists of different types', function(done) { api.get(endPoint('getHTML')+"&padID="+testPadId) .expect(function(res){ - var ehtml = res.body.data.html.replace("
    ", "").toLowerCase(); - var uhtml = ULhtml.toLowerCase(); - if(ehtml !== uhtml) throw new Error("Imported HTML does not match served HTML") + var receivedHtml = res.body.data.html.replace("
    ", "").toLowerCase(); + + if (receivedHtml !== expectedHtml) { + throw new Error(`HTML received from export is not the one we were expecting. + Received: + ${receivedHtml} + + Expected: + ${expectedHtml} + + Which is a slightly modified version of the originally imported one: + ${ulHtml}`); + } }) .expect('Content-Type', /json/) .expect(200, done) diff --git a/tests/frontend/specs/authorship_of_editions.js b/tests/frontend/specs/authorship_of_editions.js new file mode 100644 index 000000000..3bea2c46e --- /dev/null +++ b/tests/frontend/specs/authorship_of_editions.js @@ -0,0 +1,105 @@ +describe('author of pad edition', function() { + var REGULAR_LINE = 0; + var LINE_WITH_ORDERED_LIST = 1; + var LINE_WITH_UNORDERED_LIST = 2; + + // author 1 creates a new pad with some content (regular lines and lists) + before(function(done) { + var padId = helper.newPad(function() { + // make sure pad has at least 3 lines + var $firstLine = helper.padInner$('div').first(); + var threeLines = ['regular line', 'line with ordered list', 'line with unordered list'].join('
    '); + $firstLine.html(threeLines); + + // wait for lines to be processed by Etherpad + helper.waitFor(function() { + var $lineWithUnorderedList = getLine(LINE_WITH_UNORDERED_LIST); + return $lineWithUnorderedList.text() === 'line with unordered list'; + }).done(function() { + // create the unordered list + var $lineWithUnorderedList = getLine(LINE_WITH_UNORDERED_LIST); + $lineWithUnorderedList.sendkeys('{selectall}'); + + var $insertUnorderedListButton = helper.padChrome$('.buttonicon-insertunorderedlist'); + $insertUnorderedListButton.click(); + + helper.waitFor(function() { + var $lineWithUnorderedList = getLine(LINE_WITH_UNORDERED_LIST); + return $lineWithUnorderedList.find('ul li').length === 1; + }).done(function() { + // create the ordered list + var $lineWithOrderedList = getLine(LINE_WITH_ORDERED_LIST); + $lineWithOrderedList.sendkeys('{selectall}'); + + var $insertOrderedListButton = helper.padChrome$('.buttonicon-insertorderedlist'); + $insertOrderedListButton.click(); + + helper.waitFor(function() { + var $lineWithOrderedList = getLine(LINE_WITH_ORDERED_LIST); + return $lineWithOrderedList.find('ol li').length === 1; + }).done(function() { + // Reload pad, to make changes as a second user. Need a timeout here to make sure + // all changes were saved before reloading + setTimeout(function() { + helper.newPad(done, padId); + }, 1000); + }); + }); + }); + }); + this.timeout(60000); + }); + + // author 2 makes some changes on the pad + it('marks only the new content as changes of the second user on a regular line', function(done) { + changeLineAndCheckOnlyThatChangeIsFromThisAuthor(REGULAR_LINE, 'x', done); + }); + + it('marks only the new content as changes of the second user on a line with ordered list', function(done) { + changeLineAndCheckOnlyThatChangeIsFromThisAuthor(LINE_WITH_ORDERED_LIST, 'y', done); + }); + + it('marks only the new content as changes of the second user on a line with unordered list', function(done) { + changeLineAndCheckOnlyThatChangeIsFromThisAuthor(LINE_WITH_UNORDERED_LIST, 'z', done); + }); + + /* ********************** Helper functions ************************ */ + var getLine = function(lineNumber) { + return helper.padInner$('div').eq(lineNumber); + } + + var getAuthorFromClassList = function(classes) { + return classes.find(function(cls) { + return cls.startsWith('author'); + }); + } + + var changeLineAndCheckOnlyThatChangeIsFromThisAuthor = function(lineNumber, textChange, done) { + // get original author class + var classes = getLine(lineNumber).find('span').first().attr('class').split(' '); + var originalAuthor = getAuthorFromClassList(classes); + + // make change on target line + var $regularLine = getLine(lineNumber); + helper.selectLines($regularLine, $regularLine, 2, 2); // place caret after 2nd char of line + $regularLine.sendkeys(textChange); + + // wait for change to be processed by Etherpad + var otherAuthorsOfLine; + helper.waitFor(function() { + var authorsOfLine = getLine(lineNumber).find('span').map(function() { + return getAuthorFromClassList($(this).attr('class').split(' ')); + }).get(); + otherAuthorsOfLine = authorsOfLine.filter(function(author) { + return author !== originalAuthor; + }); + var lineHasChangeOfThisAuthor = otherAuthorsOfLine.length > 0; + return lineHasChangeOfThisAuthor; + }).done(function() { + var thisAuthor = otherAuthorsOfLine[0]; + var $changeOfThisAuthor = getLine(lineNumber).find('span.' + thisAuthor); + expect($changeOfThisAuthor.text()).to.be(textChange); + done(); + }); + } +}); diff --git a/tests/frontend/specs/bold.js b/tests/frontend/specs/bold.js index b54466e4e..888eb6602 100644 --- a/tests/frontend/specs/bold.js +++ b/tests/frontend/specs/bold.js @@ -44,7 +44,7 @@ describe("bold button", function(){ //select this text element $firstTextElement.sendkeys('{selectall}'); - if(inner$(window)[0].bowser.firefox || inner$(window)[0].bowser.modernIE){ // if it's a mozilla or IE + if(inner$(window)[0].bowser.modernIE){ // if it's IE var evtType = "keypress"; }else{ var evtType = "keydown"; diff --git a/tests/frontend/specs/indentation.js b/tests/frontend/specs/indentation.js index 9294cefbd..dd12fc317 100644 --- a/tests/frontend/specs/indentation.js +++ b/tests/frontend/specs/indentation.js @@ -15,7 +15,7 @@ describe("indentation button", function(){ //select this text element $firstTextElement.sendkeys('{selectall}'); - if(inner$(window)[0].bowser.firefox || inner$(window)[0].bowser.modernIE){ // if it's a mozilla or IE + if(inner$(window)[0].bowser.modernIE){ // if it's IE var evtType = "keypress"; }else{ var evtType = "keydown"; @@ -325,7 +325,7 @@ describe("indentation button", function(){ function pressEnter(){ var inner$ = helper.padInner$; - if(inner$(window)[0].bowser.firefox || inner$(window)[0].bowser.modernIE){ // if it's a mozilla or IE + if(inner$(window)[0].bowser.modernIE){ // if it's IE var evtType = "keypress"; }else{ var evtType = "keydown"; diff --git a/tests/frontend/specs/italic.js b/tests/frontend/specs/italic.js index bf7f2bc60..cecbc1808 100644 --- a/tests/frontend/specs/italic.js +++ b/tests/frontend/specs/italic.js @@ -44,7 +44,7 @@ describe("italic some text", function(){ //select this text element $firstTextElement.sendkeys('{selectall}'); - if(inner$(window)[0].bowser.firefox || inner$(window)[0].bowser.modernIE){ // if it's a mozilla or IE + if(inner$(window)[0].bowser.modernIE){ // if it's IE var evtType = "keypress"; }else{ var evtType = "keydown"; diff --git a/tests/frontend/specs/ordered_list.js b/tests/frontend/specs/ordered_list.js index 57196fefe..e7509e883 100644 --- a/tests/frontend/specs/ordered_list.js +++ b/tests/frontend/specs/ordered_list.js @@ -111,7 +111,7 @@ describe("assign ordered list", function(){ var triggerCtrlShiftShortcut = function(shortcutChar) { var inner$ = helper.padInner$; - if(inner$(window)[0].bowser.firefox || inner$(window)[0].bowser.modernIE) { // if it's a mozilla or IE + if(inner$(window)[0].bowser.modernIE) { // if it's IE var evtType = "keypress"; }else{ var evtType = "keydown"; diff --git a/tests/frontend/specs/scroll.js b/tests/frontend/specs/scroll.js index 096b06b60..94756b856 100644 --- a/tests/frontend/specs/scroll.js +++ b/tests/frontend/specs/scroll.js @@ -513,7 +513,7 @@ describe('scroll when focus line is out of viewport', function () { var pressKey = function(keyCode, shiftIsPressed){ var inner$ = helper.padInner$; var evtType; - if(inner$(window)[0].bowser.firefox || inner$(window)[0].bowser.modernIE){ // if it's a mozilla or IE + if(inner$(window)[0].bowser.modernIE){ // if it's IE evtType = 'keypress'; }else{ evtType = 'keydown'; diff --git a/tests/frontend/specs/select_formatting_buttons.js b/tests/frontend/specs/select_formatting_buttons.js index 5fb97600a..b6ec6d0c3 100644 --- a/tests/frontend/specs/select_formatting_buttons.js +++ b/tests/frontend/specs/select_formatting_buttons.js @@ -88,7 +88,7 @@ describe("select formatting buttons when selection has style applied", function( //select this text element $firstTextElement.sendkeys('{selectall}'); - if(inner$(window)[0].bowser.firefox || inner$(window)[0].bowser.modernIE){ // if it's a mozilla or IE + if(inner$(window)[0].bowser.modernIE){ // if it's IE var evtType = "keypress"; }else{ var evtType = "keydown"; diff --git a/tests/frontend/specs/undo.js b/tests/frontend/specs/undo.js index b8b7c785b..3644734f4 100644 --- a/tests/frontend/specs/undo.js +++ b/tests/frontend/specs/undo.js @@ -44,11 +44,11 @@ describe("undo button", function(){ var modifiedValue = $firstTextElement.text(); // get the modified value expect(modifiedValue).not.to.be(originalValue); // expect the value to change - if(inner$(window)[0].bowser.firefox){ // if it's a mozilla browser - var evtType = "keypress"; - }else{ - var evtType = "keydown"; - } + /* + * ACHTUNG: this is the only place in the test codebase in which a keydown + * is sent for IE. Everywhere else IE uses keypress. + */ + var evtType = "keydown"; var e = inner$.Event(evtType); e.ctrlKey = true; // Control key diff --git a/tests/frontend/travis/remote_runner.js b/tests/frontend/travis/remote_runner.js index f7b21ed52..87311d905 100644 --- a/tests/frontend/travis/remote_runner.js +++ b/tests/frontend/travis/remote_runner.js @@ -12,14 +12,13 @@ var config = { var allTestsPassed = true; var sauceTestWorker = async.queue(function (testSettings, callback) { - var browser = wd.remote(config.host, config.port, config.username, config.accessKey); - var browserChain = browser.chain(); + var browser = wd.promiseChainRemote(config.host, config.port, config.username, config.accessKey); var name = process.env.GIT_HASH + " - " + testSettings.browserName + " " + testSettings.version + ", " + testSettings.platform; testSettings.name = name; testSettings["public"] = true; testSettings["build"] = process.env.GIT_HASH; - browserChain.init(testSettings).get("http://localhost:9001/tests/frontend/", function(){ + browser.init(testSettings).get("http://localhost:9001/tests/frontend/", function(){ var url = "https://saucelabs.com/jobs/" + browser.sessionID; console.log("Remote sauce test '" + name + "' started! " + url); @@ -28,7 +27,7 @@ var sauceTestWorker = async.queue(function (testSettings, callback) { getStatusInterval && clearInterval(getStatusInterval); clearTimeout(timeout); - browserChain.quit(); + browser.quit(); if(!success){ allTestsPassed = false; @@ -39,7 +38,7 @@ var sauceTestWorker = async.queue(function (testSettings, callback) { testResult = testResult.split("\\n").map(function(line){ return "[" + testSettings.browserName + (testSettings.version === "" ? '' : (" " + testSettings.version)) + "] " + line; }).join("\n"); - + console.log(testResult); console.log("Remote sauce test '" + name + "' finished! " + url); @@ -53,7 +52,7 @@ var sauceTestWorker = async.queue(function (testSettings, callback) { var knownConsoleText = ""; var getStatusInterval = setInterval(function(){ - browserChain.eval("$('#console').text()", function(err, consoleText){ + browser.eval("$('#console').text()", function(err, consoleText){ if(!consoleText || err){ return; } @@ -68,7 +67,7 @@ var sauceTestWorker = async.queue(function (testSettings, callback) { }); }, 5); //run 5 tests in parrallel -// Firefox +// Firefox sauceTestWorker.push({ 'platform' : 'Linux' , 'browserName' : 'firefox'