diff --git a/CHANGELOG.md b/CHANGELOG.md index b39ce09ec..57613b826 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ # Changes for the next release + +### Compatibility changes +* Node.js 10.17.0 or newer is now required. + ### Notable new features * Database performance is significantly improved. @@ -118,7 +122,7 @@ * MINOR: Fix ?showChat URL param issue * MINOR: Issue where timeslider URI fails to be correct if padID is numeric * MINOR: Include prompt for clear authorship when entire document is selected -* MINOR: Include full document aText every 100 revisions to make pad restoration on database curruption achievable +* MINOR: Include full document aText every 100 revisions to make pad restoration on database corruption achievable * MINOR: Several Colibris CSS fixes * MINOR: Use mime library for mime types instead of hard-coded. * MINOR: Don't show "new pad button" if instance is read only @@ -368,7 +372,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. # 1.5.3 * NEW: Accessibility support for Screen readers, includes new fonts and keyboard shortcuts * NEW: API endpoint for Append Chat Message and Chat Backend Tests - * NEW: Error messages displayed on load are included in Default Pad Text (can be supressed) + * NEW: Error messages displayed on load are included in Default Pad Text (can be suppressed) * NEW: Content Collector can handle key values * NEW: getAttributesOnPosition Method * FIX: Firefox keeps attributes (bold etc) on cut/copy -> paste @@ -437,7 +441,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. * Fix: Timeslider UI Fix * Fix: Remove Dokuwiki * Fix: Remove long paths from windows build (stops error during extract) - * Fix: Various globals remvoed + * Fix: Various globals removed * Fix: Move all scripts into bin/ * Fix: Various CSS bugfixes for Mobile devices * Fix: Overflow Toolbar @@ -513,7 +517,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. * FIX: HTML import (don't crash on malformed or blank HTML input; strip title out of html during import) * FIX: check if uploaded file only contains ascii chars when abiword disabled * FIX: Plugin search in /admin/plugins - * FIX: Don't create new pad if a non-existant read-only pad is accessed + * FIX: Don't create new pad if a non-existent read-only pad is accessed * FIX: Drop messages from unknown connections (would lead to a crash after a restart) * FIX: API: fix createGroupFor endpoint, if mapped group is deleted * FIX: Import form for other locales @@ -530,7 +534,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. * NEW: Bump log4js for improved logging * Fix: Remove URL schemes which don't have RFC standard * Fix: Fix safeRun subsequent restarts issue - * Fix: Allow safeRun to pass arguements to run.sh + * Fix: Allow safeRun to pass arguments to run.sh * Fix: Include script for more efficient import * Fix: Fix sysv comptibile script * Fix: Fix client side changeset spamming @@ -569,7 +573,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. * Fix: Support Node 0.10 * Fix: Log HTTP on DEBUG log level * Fix: Server wont crash on import fails on 0 file import. - * Fix: Import no longer fails consistantly + * Fix: Import no longer fails consistently * Fix: Language support for non existing languages * Fix: Mobile support for chat notifications are now usable * Fix: Re-Enable Editbar buttons on reconnect @@ -601,7 +605,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. * NEW: Admin dashboard mobile device support and new hooks for Admin dashboard * NEW: Get current API version from API * NEW: CLI script to delete pads - * Fix: Automatic client reconnection on disonnect + * Fix: Automatic client reconnection on disconnect * Fix: Text Export indentation now supports multiple indentations * Fix: Bugfix getChatHistory API method * Fix: Stop Chrome losing caret after paste is texted @@ -621,7 +625,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. * Fix: Stop Opera browser inserting two new lines on enter keypress * Fix: Stop timeslider from showing NaN on pads with only one revision * Other: Allow timeslider tests to run and provide & fix various other frontend-tests - * Other: Begin dropping referene to Lite. Etherpad Lite is now named "Etherpad" + * Other: Begin dropping reference to Lite. Etherpad Lite is now named "Etherpad" * Other: Update to latest jQuery * Other: Change loading message asking user to please wait on first build * Other: Allow etherpad to use global npm installation (Safe since node 6.3) diff --git a/README.md b/README.md index 10dbd36cd..5a66ddd78 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Etherpad is a real-time collaborative editor [scalable to thousands of simultane # Installation ## Requirements -- `nodejs` >= **10.13.0**. +- `nodejs` >= **10.17.0**. ## GNU/Linux and other UNIX-like systems @@ -25,7 +25,7 @@ git clone --branch master https://github.com/ether/etherpad-lite.git && cd ether ``` ### Manual install -You'll need git and [node.js](https://nodejs.org) installed (minimum required Node version: **10.13.0**). +You'll need git and [node.js](https://nodejs.org) installed (minimum required Node version: **10.17.0**). **As any user (we recommend creating a separate user called etherpad):** diff --git a/bin/checkPadDeltas.js b/bin/checkPadDeltas.js index 45ee27d72..4900595c3 100644 --- a/bin/checkPadDeltas.js +++ b/bin/checkPadDeltas.js @@ -51,7 +51,7 @@ const util = require('util'); let atext = Changeset.makeAText('\n'); - // run trough all revisions + // run through all revisions for (const revNum of revisions) { // console.log('Fetching', revNum) const revision = await db.get(`pad:${padId}:revs:${revNum}`); diff --git a/bin/cleanRun.sh b/bin/cleanRun.sh index 57de27e5c..36dd1e384 100755 --- a/bin/cleanRun.sh +++ b/bin/cleanRun.sh @@ -3,7 +3,7 @@ # Move to the folder where ep-lite is installed cd "$(dirname "$0")"/.. -# Source constants and usefull functions +# Source constants and useful functions . bin/functions.sh #Was this script started in the bin folder? if yes move out diff --git a/bin/convert.js b/bin/convert.js deleted file mode 100644 index 47f8b2d27..000000000 --- a/bin/convert.js +++ /dev/null @@ -1,391 +0,0 @@ -const startTime = Date.now(); -const fs = require('fs'); -const ueberDB = require('../src/node_modules/ueberdb2'); -const mysql = require('../src/node_modules/ueberdb2/node_modules/mysql'); -const async = require('../src/node_modules/async'); -const Changeset = require('ep_etherpad-lite/static/js/Changeset'); -const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; -const AttributePool = require('ep_etherpad-lite/static/js/AttributePool'); - -const settingsFile = process.argv[2]; -const sqlOutputFile = process.argv[3]; - -// stop if the settings file is not set -if (!settingsFile || !sqlOutputFile) { - console.error('Use: node convert.js $SETTINGSFILE $SQLOUTPUT'); - process.exit(1); -} - -log('read settings file...'); -// read the settings file and parse the json -const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8')); -log('done'); - -log('open output file...'); -const sqlOutput = fs.openSync(sqlOutputFile, 'w'); -const sql = 'SET CHARACTER SET UTF8;\n' + - 'CREATE TABLE IF NOT EXISTS `store` ( \n' + - '`key` VARCHAR( 100 ) NOT NULL , \n' + - '`value` LONGTEXT NOT NULL , \n' + - 'PRIMARY KEY ( `key` ) \n' + - ') ENGINE = INNODB;\n' + - 'START TRANSACTION;\n\n'; -fs.writeSync(sqlOutput, sql); -log('done'); - -const etherpadDB = mysql.createConnection({ - host: settings.etherpadDB.host, - user: settings.etherpadDB.user, - password: settings.etherpadDB.password, - database: settings.etherpadDB.database, - port: settings.etherpadDB.port, -}); - -// get the timestamp once -const timestamp = Date.now(); - -let padIDs; - -async.series([ - // get all padids out of the database... - function (callback) { - log('get all padIds out of the database...'); - - etherpadDB.query('SELECT ID FROM PAD_META', [], (err, _padIDs) => { - padIDs = _padIDs; - callback(err); - }); - }, - function (callback) { - log('done'); - - // create a queue with a concurrency 100 - const queue = async.queue((padId, callback) => { - convertPad(padId, (err) => { - incrementPadStats(); - callback(err); - }); - }, 100); - - // set the step callback as the queue callback - queue.drain = callback; - - // add the padids to the worker queue - for (let i = 0, length = padIDs.length; i < length; i++) { - queue.push(padIDs[i].ID); - } - }, -], (err) => { - if (err) throw err; - - // write the groups - let sql = ''; - for (const proID in proID2groupID) { - const groupID = proID2groupID[proID]; - const subdomain = proID2subdomain[proID]; - - sql += `REPLACE INTO store VALUES (${etherpadDB.escape(`group:${groupID}`)}, ${etherpadDB.escape(JSON.stringify(groups[groupID]))});\n`; - sql += `REPLACE INTO store VALUES (${etherpadDB.escape(`mapper2group:subdomain:${subdomain}`)}, ${etherpadDB.escape(groupID)});\n`; - } - - // close transaction - sql += 'COMMIT;'; - - // end the sql file - fs.writeSync(sqlOutput, sql, undefined, 'utf-8'); - fs.closeSync(sqlOutput); - - log('finished.'); - process.exit(0); -}); - -function log(str) { - console.log(`${(Date.now() - startTime) / 1000}\t${str}`); -} - -let padsDone = 0; - -function incrementPadStats() { - padsDone++; - - if (padsDone % 100 == 0) { - const averageTime = Math.round(padsDone / ((Date.now() - startTime) / 1000)); - log(`${padsDone}/${padIDs.length}\t${averageTime} pad/s`); - } -} - -var proID2groupID = {}; -var proID2subdomain = {}; -var groups = {}; - -function convertPad(padId, callback) { - const changesets = []; - const changesetsMeta = []; - const chatMessages = []; - const authors = []; - let apool; - let subdomain; - let padmeta; - - async.series([ - // get all needed db values - function (callback) { - async.parallel([ - // get the pad revisions - function (callback) { - const sql = 'SELECT * FROM `PAD_CHAT_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_CHAT_META` WHERE ID=?)'; - - etherpadDB.query(sql, [padId], (err, results) => { - if (!err) { - try { - // parse the pages - for (let i = 0, length = results.length; i < length; i++) { - parsePage(chatMessages, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true); - } - } catch (e) { err = e; } - } - - callback(err); - }); - }, - // get the chat entries - function (callback) { - const sql = 'SELECT * FROM `PAD_REVS_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_REVS_META` WHERE ID=?)'; - - etherpadDB.query(sql, [padId], (err, results) => { - if (!err) { - try { - // parse the pages - for (let i = 0, length = results.length; i < length; i++) { - parsePage(changesets, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, false); - } - } catch (e) { err = e; } - } - - callback(err); - }); - }, - // get the pad revisions meta data - function (callback) { - const sql = 'SELECT * FROM `PAD_REVMETA_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_REVMETA_META` WHERE ID=?)'; - - etherpadDB.query(sql, [padId], (err, results) => { - if (!err) { - try { - // parse the pages - for (let i = 0, length = results.length; i < length; i++) { - parsePage(changesetsMeta, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true); - } - } catch (e) { err = e; } - } - - callback(err); - }); - }, - // get the attribute pool of this pad - function (callback) { - const sql = 'SELECT `JSON` FROM `PAD_APOOL` WHERE `ID` = ?'; - - etherpadDB.query(sql, [padId], (err, results) => { - if (!err) { - try { - apool = JSON.parse(results[0].JSON).x; - } catch (e) { err = e; } - } - - callback(err); - }); - }, - // get the authors informations - function (callback) { - const sql = 'SELECT * FROM `PAD_AUTHORS_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_AUTHORS_META` WHERE ID=?)'; - - etherpadDB.query(sql, [padId], (err, results) => { - if (!err) { - try { - // parse the pages - for (let i = 0, length = results.length; i < length; i++) { - parsePage(authors, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true); - } - } catch (e) { err = e; } - } - - callback(err); - }); - }, - // get the pad information - function (callback) { - const sql = 'SELECT JSON FROM `PAD_META` WHERE ID=?'; - - etherpadDB.query(sql, [padId], (err, results) => { - if (!err) { - try { - padmeta = JSON.parse(results[0].JSON).x; - } catch (e) { err = e; } - } - - callback(err); - }); - }, - // get the subdomain - function (callback) { - // skip if this is no proPad - if (padId.indexOf('$') == -1) { - callback(); - return; - } - - // get the proID out of this padID - const proID = padId.split('$')[0]; - - const sql = 'SELECT subDomain FROM pro_domains WHERE ID = ?'; - - etherpadDB.query(sql, [proID], (err, results) => { - if (!err) { - subdomain = results[0].subDomain; - } - - callback(err); - }); - }, - ], callback); - }, - function (callback) { - // saves all values that should be written to the database - const values = {}; - - // this is a pro pad, let's convert it to a group pad - if (padId.indexOf('$') != -1) { - const padIdParts = padId.split('$'); - const proID = padIdParts[0]; - const padName = padIdParts[1]; - - let groupID; - - // this proID is not converted so far, do it - if (proID2groupID[proID] == null) { - groupID = `g.${randomString(16)}`; - - // create the mappers for this new group - proID2groupID[proID] = groupID; - proID2subdomain[proID] = subdomain; - groups[groupID] = {pads: {}}; - } - - // use the generated groupID; - groupID = proID2groupID[proID]; - - // rename the pad - padId = `${groupID}$${padName}`; - - // set the value for this pad in the group - groups[groupID].pads[padId] = 1; - } - - try { - const newAuthorIDs = {}; - const oldName2newName = {}; - - // replace the authors with generated authors - // we need to do that cause where the original etherpad saves pad local authors, the new (lite) etherpad uses them global - for (var i in apool.numToAttrib) { - var key = apool.numToAttrib[i][0]; - const value = apool.numToAttrib[i][1]; - - // skip non authors and anonymous authors - if (key != 'author' || value == '') continue; - - // generate new author values - const authorID = `a.${randomString(16)}`; - const authorColorID = authors[i].colorId || Math.floor(Math.random() * (exports.getColorPalette().length)); - const authorName = authors[i].name || null; - - // overwrite the authorID of the attribute pool - apool.numToAttrib[i][1] = authorID; - - // write the author to the database - values[`globalAuthor:${authorID}`] = {colorId: authorColorID, name: authorName, timestamp}; - - // save in mappers - newAuthorIDs[i] = authorID; - oldName2newName[value] = authorID; - } - - // save all revisions - for (var i = 0; i < changesets.length; i++) { - values[`pad:${padId}:revs:${i}`] = {changeset: changesets[i], - meta: { - author: newAuthorIDs[changesetsMeta[i].a], - timestamp: changesetsMeta[i].t, - atext: changesetsMeta[i].atext || undefined, - }}; - } - - // save all chat messages - for (var i = 0; i < chatMessages.length; i++) { - values[`pad:${padId}:chat:${i}`] = {text: chatMessages[i].lineText, - userId: oldName2newName[chatMessages[i].userId], - time: chatMessages[i].time}; - } - - // generate the latest atext - const fullAPool = (new AttributePool()).fromJsonable(apool); - const keyRev = Math.floor(padmeta.head / padmeta.keyRevInterval) * padmeta.keyRevInterval; - let atext = changesetsMeta[keyRev].atext; - let curRev = keyRev; - while (curRev < padmeta.head) { - curRev++; - const changeset = changesets[curRev]; - atext = Changeset.applyToAText(changeset, atext, fullAPool); - } - - values[`pad:${padId}`] = {atext, - pool: apool, - head: padmeta.head, - chatHead: padmeta.numChatMessages}; - } catch (e) { - console.error(`Error while converting pad ${padId}, pad skipped`); - console.error(e.stack ? e.stack : JSON.stringify(e)); - callback(); - return; - } - - let sql = ''; - for (var key in values) { - sql += `REPLACE INTO store VALUES (${etherpadDB.escape(key)}, ${etherpadDB.escape(JSON.stringify(values[key]))});\n`; - } - - fs.writeSync(sqlOutput, sql, undefined, 'utf-8'); - callback(); - }, - ], callback); -} - -/** - * This parses a Page like Etherpad uses them in the databases - * The offsets describes the length of a unit in the page, the data are - * all values behind each other - */ -function parsePage(array, pageStart, offsets, data, json) { - let start = 0; - const lengths = offsets.split(','); - - for (let i = 0; i < lengths.length; i++) { - let unitLength = lengths[i]; - - // skip empty units - if (unitLength == '') continue; - - // parse the number - unitLength = Number(unitLength); - - // cut the unit out of data - const unit = data.substr(start, unitLength); - - // put it into the array - array[pageStart + i] = json ? JSON.parse(unit) : unit; - - // update start - start += unitLength; - } -} diff --git a/bin/debugRun.sh b/bin/debugRun.sh index 9b2fff9bd..e04db88d0 100755 --- a/bin/debugRun.sh +++ b/bin/debugRun.sh @@ -3,7 +3,7 @@ # Move to the folder where ep-lite is installed cd "$(dirname "$0")"/.. -# Source constants and usefull functions +# Source constants and useful functions . bin/functions.sh # Prepare the environment diff --git a/bin/doc/generate.js b/bin/doc/generate.js index 803f5017e..d04468a8b 100644 --- a/bin/doc/generate.js +++ b/bin/doc/generate.js @@ -1,4 +1,7 @@ #!/usr/bin/env node + +'use strict'; + // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a @@ -20,7 +23,6 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -const marked = require('marked'); const fs = require('fs'); const path = require('path'); @@ -33,12 +35,12 @@ let template = null; let inputFile = null; args.forEach((arg) => { - if (!arg.match(/^\-\-/)) { + if (!arg.match(/^--/)) { inputFile = arg; - } else if (arg.match(/^\-\-format=/)) { - format = arg.replace(/^\-\-format=/, ''); - } else if (arg.match(/^\-\-template=/)) { - template = arg.replace(/^\-\-template=/, ''); + } else if (arg.match(/^--format=/)) { + format = arg.replace(/^--format=/, ''); + } else if (arg.match(/^--template=/)) { + template = arg.replace(/^--template=/, ''); } }); @@ -56,11 +58,11 @@ fs.readFile(inputFile, 'utf8', (er, input) => { }); -const includeExpr = /^@include\s+([A-Za-z0-9-_\/]+)(?:\.)?([a-zA-Z]*)$/gmi; +const includeExpr = /^@include\s+([A-Za-z0-9-_/]+)(?:\.)?([a-zA-Z]*)$/gmi; const includeData = {}; -function processIncludes(inputFile, input, cb) { +const processIncludes = (inputFile, input, cb) => { const includes = input.match(includeExpr); - if (includes === null) return cb(null, input); + if (includes == null) return cb(null, input); let errState = null; console.error(includes); let incCount = includes.length; @@ -70,7 +72,7 @@ function processIncludes(inputFile, input, cb) { let fname = include.replace(/^@include\s+/, ''); if (!fname.match(/\.md$/)) fname += '.md'; - if (includeData.hasOwnProperty(fname)) { + if (Object.prototype.hasOwnProperty.call(includeData, fname)) { input = input.split(include).join(includeData[fname]); incCount--; if (incCount === 0) { @@ -94,10 +96,10 @@ function processIncludes(inputFile, input, cb) { }); }); }); -} +}; -function next(er, input) { +const next = (er, input) => { if (er) throw er; switch (format) { case 'json': @@ -117,4 +119,4 @@ function next(er, input) { default: throw new Error(`Invalid format: ${format}`); } -} +}; diff --git a/bin/doc/html.js b/bin/doc/html.js index 26cf3f185..2c38aec23 100644 --- a/bin/doc/html.js +++ b/bin/doc/html.js @@ -1,3 +1,5 @@ +'use strict'; + // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a @@ -23,17 +25,17 @@ const fs = require('fs'); const marked = require('marked'); const path = require('path'); -module.exports = toHTML; -function toHTML(input, filename, template, cb) { +const toHTML = (input, filename, template, cb) => { const lexed = marked.lexer(input); fs.readFile(template, 'utf8', (er, template) => { if (er) return cb(er); render(lexed, filename, template, cb); }); -} +}; +module.exports = toHTML; -function render(lexed, filename, template, cb) { +const render = (lexed, filename, template, cb) => { // get the section const section = getSection(lexed); @@ -52,23 +54,23 @@ function render(lexed, filename, template, cb) { // content has to be the last thing we do with // the lexed tokens, because it's destructive. - content = marked.parser(lexed); + const content = marked.parser(lexed); template = template.replace(/__CONTENT__/g, content); cb(null, template); }); -} +}; // just update the list item text in-place. // lists that come right after a heading are what we're after. -function parseLists(input) { +const parseLists = (input) => { let state = null; let depth = 0; const output = []; output.links = input.links; input.forEach((tok) => { - if (state === null) { + if (state == null) { if (tok.type === 'heading') { state = 'AFTERHEADING'; } @@ -112,29 +114,27 @@ function parseLists(input) { }); return output; -} +}; -function parseListItem(text) { - text = text.replace(/\{([^\}]+)\}/, '$1'); +const parseListItem = (text) => { + text = text.replace(/\{([^}]+)\}/, '$1'); // XXX maybe put more stuff here? return text; -} +}; // section is just the first heading -function getSection(lexed) { - const section = ''; +const getSection = (lexed) => { for (let i = 0, l = lexed.length; i < l; i++) { const tok = lexed[i]; if (tok.type === 'heading') return tok.text; } return ''; -} +}; -function buildToc(lexed, filename, cb) { - const indent = 0; +const buildToc = (lexed, filename, cb) => { let toc = []; let depth = 0; lexed.forEach((tok) => { @@ -155,18 +155,18 @@ function buildToc(lexed, filename, cb) { toc = marked.parse(toc.join('\n')); cb(null, toc); -} +}; const idCounters = {}; -function getId(text) { +const getId = (text) => { text = text.toLowerCase(); text = text.replace(/[^a-z0-9]+/g, '_'); text = text.replace(/^_+|_+$/, ''); text = text.replace(/^([^a-z])/, '_$1'); - if (idCounters.hasOwnProperty(text)) { + if (Object.prototype.hasOwnProperty.call(idCounters, text)) { text += `_${++idCounters[text]}`; } else { idCounters[text] = 0; } return text; -} +}; diff --git a/bin/doc/json.js b/bin/doc/json.js index 3ce62a301..1a5ecb1d8 100644 --- a/bin/doc/json.js +++ b/bin/doc/json.js @@ -1,3 +1,4 @@ +'use strict'; // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a @@ -26,7 +27,7 @@ module.exports = doJSON; const marked = require('marked'); -function doJSON(input, filename, cb) { +const doJSON = (input, filename, cb) => { const root = {source: filename}; const stack = [root]; let depth = 0; @@ -40,7 +41,7 @@ function doJSON(input, filename, cb) { // // This is for cases where the markdown semantic structure is lacking. if (type === 'paragraph' || type === 'html') { - const metaExpr = /\n*/g; + const metaExpr = /\n*/g; text = text.replace(metaExpr, (_0, k, v) => { current[k.trim()] = v.trim(); return ''; @@ -146,7 +147,7 @@ function doJSON(input, filename, cb) { } return cb(null, root); -} +}; // go from something like this: @@ -191,7 +192,7 @@ function doJSON(input, filename, cb) { // desc: 'whether or not to send output to parent\'s stdio.', // default: 'false' } ] } ] -function processList(section) { +const processList = (section) => { const list = section.list; const values = []; let current; @@ -203,13 +204,13 @@ function processList(section) { if (type === 'space') return; if (type === 'list_item_start') { if (!current) { - var n = {}; + const n = {}; values.push(n); current = n; } else { current.options = current.options || []; stack.push(current); - var n = {}; + const n = {}; current.options.push(n); current = n; } @@ -247,11 +248,11 @@ function processList(section) { switch (section.type) { case 'ctor': case 'classMethod': - case 'method': + case 'method': { // each item is an argument, unless the name is 'return', // in which case it's the return value. section.signatures = section.signatures || []; - var sig = {}; + const sig = {}; section.signatures.push(sig); sig.params = values.filter((v) => { if (v.name === 'return') { @@ -262,11 +263,11 @@ function processList(section) { }); parseSignature(section.textRaw, sig); break; - - case 'property': + } + case 'property': { // there should be only one item, which is the value. // copy the data up to the section. - var value = values[0] || {}; + const value = values[0] || {}; delete value.name; section.typeof = value.type; delete value.type; @@ -274,20 +275,21 @@ function processList(section) { section[k] = value[k]; }); break; + } - case 'event': + case 'event': { // event: each item is an argument. section.params = values; break; + } } - // section.listParsed = values; delete section.list; -} +}; // textRaw = "someobject.someMethod(a, [b=100], [c])" -function parseSignature(text, sig) { +const parseSignature = (text, sig) => { let params = text.match(paramExpr); if (!params) return; params = params[1]; @@ -322,10 +324,10 @@ function parseSignature(text, sig) { if (optional) param.optional = true; if (def !== undefined) param.default = def; }); -} +}; -function parseListItem(item) { +const parseListItem = (item) => { if (item.options) item.options.forEach(parseListItem); if (!item.textRaw) return; @@ -341,7 +343,7 @@ function parseListItem(item) { item.name = 'return'; text = text.replace(retExpr, ''); } else { - const nameExpr = /^['`"]?([^'`": \{]+)['`"]?\s*:?\s*/; + const nameExpr = /^['`"]?([^'`": {]+)['`"]?\s*:?\s*/; const name = text.match(nameExpr); if (name) { item.name = name[1]; @@ -358,7 +360,7 @@ function parseListItem(item) { } text = text.trim(); - const typeExpr = /^\{([^\}]+)\}/; + const typeExpr = /^\{([^}]+)\}/; const type = text.match(typeExpr); if (type) { item.type = type[1]; @@ -376,10 +378,10 @@ function parseListItem(item) { text = text.replace(/^\s*-\s*/, ''); text = text.trim(); if (text) item.desc = text; -} +}; -function finishSection(section, parent) { +const finishSection = (section, parent) => { if (!section || !parent) { throw new Error(`Invalid finishSection call\n${ JSON.stringify(section)}\n${ @@ -416,7 +418,7 @@ function finishSection(section, parent) { ctor.signatures.forEach((sig) => { sig.desc = ctor.desc; }); - sigs.push.apply(sigs, ctor.signatures); + sigs.push(...ctor.signatures); }); delete section.ctors; } @@ -479,50 +481,50 @@ function finishSection(section, parent) { parent[plur] = parent[plur] || []; parent[plur].push(section); -} +}; // Not a general purpose deep copy. // But sufficient for these basic things. -function deepCopy(src, dest) { - Object.keys(src).filter((k) => !dest.hasOwnProperty(k)).forEach((k) => { +const deepCopy = (src, dest) => { + Object.keys(src).filter((k) => !Object.prototype.hasOwnProperty.call(dest, k)).forEach((k) => { dest[k] = deepCopy_(src[k]); }); -} +}; -function deepCopy_(src) { +const deepCopy_ = (src) => { if (!src) return src; if (Array.isArray(src)) { - var c = new Array(src.length); + const c = new Array(src.length); src.forEach((v, i) => { c[i] = deepCopy_(v); }); return c; } if (typeof src === 'object') { - var c = {}; + const c = {}; Object.keys(src).forEach((k) => { c[k] = deepCopy_(src[k]); }); return c; } return src; -} +}; // these parse out the contents of an H# tag const eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i; const classExpr = /^Class:\s*([^ ]+).*?$/i; -const propExpr = /^(?:property:?\s*)?[^\.]+\.([^ \.\(\)]+)\s*?$/i; -const braceExpr = /^(?:property:?\s*)?[^\.\[]+(\[[^\]]+\])\s*?$/i; +const propExpr = /^(?:property:?\s*)?[^.]+\.([^ .()]+)\s*?$/i; +const braceExpr = /^(?:property:?\s*)?[^.[]+(\[[^\]]+\])\s*?$/i; const classMethExpr = - /^class\s*method\s*:?[^\.]+\.([^ \.\(\)]+)\([^\)]*\)\s*?$/i; + /^class\s*method\s*:?[^.]+\.([^ .()]+)\([^)]*\)\s*?$/i; const methExpr = - /^(?:method:?\s*)?(?:[^\.]+\.)?([^ \.\(\)]+)\([^\)]*\)\s*?$/i; -const newExpr = /^new ([A-Z][a-z]+)\([^\)]*\)\s*?$/; -var paramExpr = /\((.*)\);?$/; + /^(?:method:?\s*)?(?:[^.]+\.)?([^ .()]+)\([^)]*\)\s*?$/i; +const newExpr = /^new ([A-Z][a-z]+)\([^)]*\)\s*?$/; +const paramExpr = /\((.*)\);?$/; -function newSection(tok) { +const newSection = (tok) => { const section = {}; // infer the type from the text. const text = section.textRaw = tok.text; @@ -551,4 +553,4 @@ function newSection(tok) { section.name = text; } return section; -} +}; diff --git a/bin/doc/package.json b/bin/doc/package.json index 1a29f1b1c..2f027616c 100644 --- a/bin/doc/package.json +++ b/bin/doc/package.json @@ -4,7 +4,7 @@ "description": "Internal tool for generating Node.js API docs", "version": "0.0.0", "engines": { - "node": ">=0.6.10" + "node": ">=10.17.0" }, "dependencies": { "marked": "0.8.2" diff --git a/bin/fastRun.sh b/bin/fastRun.sh index 90d83dc8e..63524e793 100755 --- a/bin/fastRun.sh +++ b/bin/fastRun.sh @@ -12,7 +12,7 @@ set -eu # source: https://stackoverflow.com/questions/59895/how-to-get-the-source-directory-of-a-bash-script-from-within-the-script-itself#246128 DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -# Source constants and usefull functions +# Source constants and useful functions . ${DIR}/../bin/functions.sh echo "Running directly, without checking/installing dependencies" diff --git a/bin/installDeps.sh b/bin/installDeps.sh index bdce38fc7..be3f1fd8f 100755 --- a/bin/installDeps.sh +++ b/bin/installDeps.sh @@ -3,7 +3,7 @@ # Move to the folder where ep-lite is installed cd "$(dirname "$0")"/.. -# Source constants and usefull functions +# Source constants and useful functions . bin/functions.sh # Is node installed? diff --git a/bin/plugins/checkPlugin.js b/bin/plugins/checkPlugin.js index cbe146aa1..0b736af80 100755 --- a/bin/plugins/checkPlugin.js +++ b/bin/plugins/checkPlugin.js @@ -263,7 +263,7 @@ fs.readdir(pluginPath, (err, rootFiles) => { console.warn('No engines or node engine in package.json'); if (autoFix) { const engines = { - node: '>=10.13.0', + node: '^10.17.0 || >=11.14.0', }; parsedPackageJSON.engines = engines; writePackageJson(parsedPackageJSON); diff --git a/bin/repairPad.js b/bin/repairPad.js index ff2da9776..ce0c6072e 100644 --- a/bin/repairPad.js +++ b/bin/repairPad.js @@ -23,7 +23,7 @@ const util = require('util'); (async () => { await util.promisify(npm.load)({}); - // intialize database + // initialize database require('ep_etherpad-lite/node/utils/Settings'); const db = require('ep_etherpad-lite/node/db/DB'); await db.init(); diff --git a/bin/run.sh b/bin/run.sh index 50bce4bdd..c10359904 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -3,7 +3,7 @@ # Move to the folder where ep-lite is installed cd "$(dirname "$0")"/.. -# Source constants and usefull functions +# Source constants and useful functions . bin/functions.sh ignoreRoot=0 diff --git a/doc/api/http_api.md b/doc/api/http_api.md index fb570a393..0cfc85a07 100644 --- a/doc/api/http_api.md +++ b/doc/api/http_api.md @@ -263,7 +263,7 @@ deletes a session #### getSessionInfo(sessionID) * API >= 1 -returns informations about a session +returns information about a session *Example returns:* * `{code: 0, message:"ok", data: {authorID: "a.s8oes9dhwrvt0zif", groupID: g.s8oes9dhwrvt0zif, validUntil: 1312201246}}` diff --git a/doc/localization.md b/doc/localization.md index 54675e2da..d047944ff 100644 --- a/doc/localization.md +++ b/doc/localization.md @@ -95,7 +95,7 @@ For example, if you want to replace `Chat` with `Notes`, simply add... ## Customization for Administrators -As an Etherpad administrator, it is possible to overwrite core mesages as well as messages in plugins. These include error messages, labels, and user instructions. Whereas the localization in the source code is in separate files separated by locale, an administrator's custom localizations are in `settings.json` under the `customLocaleStrings` key, with each locale separated by a sub-key underneath. +As an Etherpad administrator, it is possible to overwrite core messages as well as messages in plugins. These include error messages, labels, and user instructions. Whereas the localization in the source code is in separate files separated by locale, an administrator's custom localizations are in `settings.json` under the `customLocaleStrings` key, with each locale separated by a sub-key underneath. For example, let's say you want to change the text on the "New Pad" button on Etherpad's home page. If you look in `locales/en.json` (or `locales/en-gb.json`) you'll see the key for this text is `"index.newPad"`. You could add the following to `settings.json`: diff --git a/doc/plugins.md b/doc/plugins.md index 2062378bb..d8239c68a 100644 --- a/doc/plugins.md +++ b/doc/plugins.md @@ -225,7 +225,7 @@ publish your plugin. "author": "USERNAME (REAL NAME) ", "contributors": [], "dependencies": {"MODULE": "0.3.20"}, - "engines": { "node": ">= 10.13.0"} + "engines": { "node": "^10.17.0 || >=11.14.0"} } ``` diff --git a/package.json b/package.json index 41e472831..de9298807 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,9 @@ "tests/**/*" ], "excludedFiles": [ - "**/.eslintrc.js" + "**/.eslintrc.js", + "tests/frontend/travis/**/*", + "tests/ratelimit/**/*" ], "extends": "etherpad/tests", "rules": { @@ -75,7 +77,8 @@ "tests/frontend/**/*" ], "excludedFiles": [ - "**/.eslintrc.js" + "**/.eslintrc.js", + "tests/frontend/travis/**/*" ], "extends": "etherpad/tests/frontend", "overrides": [ @@ -92,6 +95,12 @@ } } ] + }, + { + "files": [ + "tests/frontend/travis/**/*" + ], + "extends": "etherpad/node" } ], "root": true @@ -100,6 +109,6 @@ "lint": "eslint ." }, "engines": { - "node": ">=10.13.0" + "node": "^10.17.0 || >=11.14.0" } } diff --git a/settings.json.docker b/settings.json.docker index 081af38b8..6938b2713 100644 --- a/settings.json.docker +++ b/settings.json.docker @@ -171,7 +171,7 @@ * * * Database specific settings are dependent on dbType, and go in dbSettings. - * Remember that since Etherpad 1.6.0 you can also store these informations in + * Remember that since Etherpad 1.6.0 you can also store this information in * credentials.json. * * For a complete list of the supported drivers, please refer to: diff --git a/settings.json.template b/settings.json.template index f4cfdea62..3c9c50837 100644 --- a/settings.json.template +++ b/settings.json.template @@ -162,7 +162,7 @@ * * * Database specific settings are dependent on dbType, and go in dbSettings. - * Remember that since Etherpad 1.6.0 you can also store these informations in + * Remember that since Etherpad 1.6.0 you can also store this information in * credentials.json. * * For a complete list of the supported drivers, please refer to: diff --git a/src/locales/gl.json b/src/locales/gl.json index 406c99521..02a04f563 100644 --- a/src/locales/gl.json +++ b/src/locales/gl.json @@ -2,12 +2,47 @@ "@metadata": { "authors": [ "Elisardojm", + "Ghose", "Toliño" ] }, + "admin.page-title": "Panel de administración - Etherpad", + "admin_plugins": "Xestor de complementos", + "admin_plugins.available": "Complementos dispoñibles", + "admin_plugins.available_not-found": "Non se atopan complementos.", + "admin_plugins.available_fetching": "Obtendo...", + "admin_plugins.available_install.value": "Instalar", + "admin_plugins.available_search.placeholder": "Buscar complementos para instalar", + "admin_plugins.description": "Descrición", + "admin_plugins.installed": "Complementos instalados", + "admin_plugins.installed_fetching": "Obtendo os complementos instalados...", + "admin_plugins.installed_nothing": "Aínda non instalaches ningún complemento.", + "admin_plugins.installed_uninstall.value": "Desinstalar", + "admin_plugins.last-update": "Última actualización", + "admin_plugins.name": "Nome", + "admin_plugins.page-title": "Xestos de complementos - Etherpad", + "admin_plugins.version": "Versión", + "admin_plugins_info": "Información para resolver problemas", + "admin_plugins_info.hooks": "Ganchos instalados", + "admin_plugins_info.hooks_client": "Ganchos do lado do cliente", + "admin_plugins_info.hooks_server": "Ganchos do lado do servidor", + "admin_plugins_info.parts": "Partes instaladas", + "admin_plugins_info.plugins": "Complementos instalados", + "admin_plugins_info.page-title": "Información do complemento - Etherpad", + "admin_plugins_info.version": "Versión de Etherpad", + "admin_plugins_info.version_latest": "Última versión dispoñible", + "admin_plugins_info.version_number": "Número da versión", + "admin_settings": "Axustes", + "admin_settings.current": "Configuración actual", + "admin_settings.current_example-devel": "Modelo de exemplo dos axustes de desenvolvemento", + "admin_settings.current_example-prod": "Modelo de exemplo dos axustes en produción", + "admin_settings.current_restart.value": "Reiniciar Etherpad", + "admin_settings.current_save.value": "Gardar axustes", + "admin_settings.page-title": "Axustes - Etherpad", "index.newPad": "Novo documento", - "index.createOpenPad": "ou cree/abra un documento co nome:", - "pad.toolbar.bold.title": "Negra (Ctrl-B)", + "index.createOpenPad": "ou crea/abre un documento co nome:", + "index.openPad": "abrir un Pad existente co nome:", + "pad.toolbar.bold.title": "Resaltado (Ctrl-B)", "pad.toolbar.italic.title": "Cursiva (Ctrl-I)", "pad.toolbar.underline.title": "Subliñar (Ctrl-U)", "pad.toolbar.strikethrough.title": "Riscar (Ctrl+5)", @@ -17,28 +52,30 @@ "pad.toolbar.unindent.title": "Sen sangría (Maiús.+TAB)", "pad.toolbar.undo.title": "Desfacer (Ctrl-Z)", "pad.toolbar.redo.title": "Refacer (Ctrl-Y)", - "pad.toolbar.clearAuthorship.title": "Limpar as cores de identificación dos autores (Ctrl+Shift+C)", + "pad.toolbar.clearAuthorship.title": "Eliminar as cores que identifican ás autoras (Ctrl+Shift+C)", "pad.toolbar.import_export.title": "Importar/Exportar desde/a diferentes formatos de ficheiro", "pad.toolbar.timeslider.title": "Liña do tempo", "pad.toolbar.savedRevision.title": "Gardar a revisión", - "pad.toolbar.settings.title": "Configuracións", + "pad.toolbar.settings.title": "Axustes", "pad.toolbar.embed.title": "Compartir e incorporar este documento", - "pad.toolbar.showusers.title": "Mostrar os usuarios deste documento", + "pad.toolbar.showusers.title": "Mostrar as usuarias deste documento", "pad.colorpicker.save": "Gardar", "pad.colorpicker.cancel": "Cancelar", "pad.loading": "Cargando...", - "pad.noCookie": "Non se puido atopar a cookie. Por favor, habilite as cookies no seu navegador!", - "pad.permissionDenied": "Non ten permiso para acceder a este documento", + "pad.noCookie": "Non se puido atopar a cookie. Por favor, habilita as cookies no teu navegador! A túa sesión e axustes non se gardarán entre visitas. Esto podería deberse a que Etherpad está incluído nalgún iFrame nalgúns navegadores. Asegúrate de que Etherpad está no mesmo subdominio/dominio que o iFrame pai", + "pad.permissionDenied": "Non tes permiso para acceder a este documento", "pad.settings.padSettings": "Configuracións do documento", "pad.settings.myView": "A miña vista", "pad.settings.stickychat": "Chat sempre visible", "pad.settings.chatandusers": "Mostrar o chat e os usuarios", "pad.settings.colorcheck": "Cores de identificación", "pad.settings.linenocheck": "Números de liña", - "pad.settings.rtlcheck": "Quere ler o contido da dereita á esquerda?", + "pad.settings.rtlcheck": "Queres ler o contido da dereita á esquerda?", "pad.settings.fontType": "Tipo de letra:", "pad.settings.fontType.normal": "Normal", "pad.settings.language": "Lingua:", + "pad.settings.about": "Acerca de", + "pad.settings.poweredBy": "Grazas a", "pad.importExport.import_export": "Importar/Exportar", "pad.importExport.import": "Cargar un ficheiro de texto ou documento", "pad.importExport.importSuccessful": "Correcto!", @@ -49,9 +86,9 @@ "pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (Open Document Format)", - "pad.importExport.abiword.innerHTML": "Só pode importar texto simple ou formatos HTML. Para obter máis información sobre as características de importación avanzadas instale AbiWord.", + "pad.importExport.abiword.innerHTML": "Só podes importar texto simple ou formatos HTML. Para obter máis información sobre as características de importación avanzadas instala AbiWord.", "pad.modals.connected": "Conectado.", - "pad.modals.reconnecting": "Reconectando co seu documento...", + "pad.modals.reconnecting": "Reconectando co teu documento...", "pad.modals.forcereconnect": "Forzar a reconexión", "pad.modals.reconnecttimer": "Intentarase reconectar en", "pad.modals.cancel": "Cancelar", @@ -73,6 +110,10 @@ "pad.modals.corruptPad.cause": "Isto pode deberse a unha cofiguración errónea do servidor ou algún outro comportamento inesperado. Póñase en contacto co administrador do servizo.", "pad.modals.deleted": "Borrado.", "pad.modals.deleted.explanation": "Este documento foi eliminado.", + "pad.modals.rateLimited": "Taxa limitada.", + "pad.modals.rateLimited.explanation": "Enviaches demasiadas mensaxes a este documento polo que te desconectamos.", + "pad.modals.rejected.explanation": "O servidor rexeitou unha mensaxe que o teu navegador enviou.", + "pad.modals.rejected.cause": "O servidor podería ter sido actualizado mentras ollabas o documento, ou pode que sexa un fallo de Etherpad. Intenta recargar a páxina.", "pad.modals.disconnected": "Foi desconectado.", "pad.modals.disconnected.explanation": "Perdeuse a conexión co servidor", "pad.modals.disconnected.cause": "O servidor non está dispoñible. Póñase en contacto co administrador do servizo se o problema continúa.", @@ -83,6 +124,9 @@ "pad.chat": "Chat", "pad.chat.title": "Abrir o chat deste documento.", "pad.chat.loadmessages": "Cargar máis mensaxes", + "pad.chat.stick.title": "Pegar a conversa á pantalla", + "pad.chat.writeMessage.placeholder": "Escribe aquí a túa mensaxe", + "timeslider.followContents": "Segue as actualizacións do contido", "timeslider.pageTitle": "Liña do tempo de {{appTitle}}", "timeslider.toolbar.returnbutton": "Volver ao documento", "timeslider.toolbar.authors": "Autores:", @@ -112,7 +156,7 @@ "pad.savedrevs.timeslider": "Pode consultar as revisións gardadas visitando a liña do tempo", "pad.userlist.entername": "Insira o seu nome", "pad.userlist.unnamed": "anónimo", - "pad.editbar.clearcolors": "Quere limpar as cores de identificación dos autores en todo o documento?", + "pad.editbar.clearcolors": "Eliminar as cores relativas aos autores en todo o documento? Non se poderán recuperar", "pad.impexp.importbutton": "Importar agora", "pad.impexp.importing": "Importando...", "pad.impexp.confirmimport": "A importación dun ficheiro ha sobrescribir o texto actual do documento. Está seguro de querer continuar?", @@ -121,5 +165,6 @@ "pad.impexp.uploadFailed": "Houbo un erro ao cargar o ficheiro; inténteo de novo", "pad.impexp.importfailed": "Fallou a importación", "pad.impexp.copypaste": "Copie e pegue", - "pad.impexp.exportdisabled": "A exportación en formato {{type}} está desactivada. Póñase en contacto co administrador do sistema se quere máis detalles." + "pad.impexp.exportdisabled": "A exportación en formato {{type}} está desactivada. Póñase en contacto co administrador do sistema se quere máis detalles.", + "pad.impexp.maxFileSize": "Ficheiro demasiado granda. Contacta coa administración para aumentar o tamaño permitido para importacións" } diff --git a/src/locales/pt.json b/src/locales/pt.json index d9faa3ba0..82972333e 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -4,6 +4,7 @@ "Athena in Wonderland", "Cainamarques", "GoEThe", + "Guilha", "Hamilton Abreu", "Imperadeiro98", "Luckas", @@ -16,9 +17,42 @@ "Waldyrious" ] }, + "admin.page-title": "Painel do administrador - Etherpad", + "admin_plugins": "Gestor de plugins", + "admin_plugins.available": "Plugins disponíveis", + "admin_plugins.available_not-found": "Não foram encontrados plugins.", + "admin_plugins.available_fetching": "A obter...", + "admin_plugins.available_install.value": "Instalar", + "admin_plugins.available_search.placeholder": "Procura plugins para instalar", + "admin_plugins.description": "Descrição", + "admin_plugins.installed": "Plugins instalados", + "admin_plugins.installed_fetching": "A obter plugins instalados...", + "admin_plugins.installed_nothing": "Não instalas-te nenhum plugin ainda.", + "admin_plugins.installed_uninstall.value": "Desinstalar", + "admin_plugins.last-update": "Ultima atualização", + "admin_plugins.name": "Nome", + "admin_plugins.page-title": "Gestor de plugins - Etherpad", + "admin_plugins.version": "Versão", + "admin_plugins_info": "Informação de resolução de problemas", + "admin_plugins_info.hooks": "Hooks instalados", + "admin_plugins_info.hooks_client": "Hooks do lado-do-cliente", + "admin_plugins_info.hooks_server": "Hooks do lado-do-servidor", + "admin_plugins_info.parts": "Partes instaladas", + "admin_plugins_info.plugins": "Plugins instalados", + "admin_plugins_info.page-title": "Informação do plugin - Etherpad", + "admin_plugins_info.version": "Versão do Etherpad", + "admin_plugins_info.version_latest": "Última versão disponível", + "admin_plugins_info.version_number": "Número de versão", + "admin_settings": "Definições", + "admin_settings.current": "Configuração atual", + "admin_settings.current_example-devel": "Exemplo do modo de Desenvolvedor", + "admin_settings.current_example-prod": "Exemplo do modo de Produção", + "admin_settings.current_restart.value": "Reiniciar Etherpad", + "admin_settings.current_save.value": "Guardar Definições", + "admin_settings.page-title": "Definições - Etherpad", "index.newPad": "Nova Nota", - "index.createOpenPad": "ou crie/abra uma nota com o nome:", - "index.openPad": "abrir uma «Nota» existente com o nome:", + "index.createOpenPad": "ou cria/abre uma nota com o nome:", + "index.openPad": "abrir uma Nota existente com o nome:", "pad.toolbar.bold.title": "Negrito (Ctrl+B)", "pad.toolbar.italic.title": "Itálico (Ctrl+I)", "pad.toolbar.underline.title": "Sublinhado (Ctrl+U)", @@ -26,7 +60,7 @@ "pad.toolbar.ol.title": "Lista ordenada (Ctrl+Shift+N)", "pad.toolbar.ul.title": "Lista desordenada (Ctrl+Shift+L)", "pad.toolbar.indent.title": "Indentar (TAB)", - "pad.toolbar.unindent.title": "Remover indentação (Shift+TAB)", + "pad.toolbar.unindent.title": "Indentação (Shift+TAB)", "pad.toolbar.undo.title": "Desfazer (Ctrl+Z)", "pad.toolbar.redo.title": "Refazer (Ctrl+Y)", "pad.toolbar.clearAuthorship.title": "Limpar cores de autoria (Ctrl+Shift+C)", @@ -65,7 +99,7 @@ "pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.abiword.innerHTML": "Só pode fazer importações de texto não formatado ou com formato HTML. Para funcionalidades de importação de texto mais avançadas, instale AbiWord ou LibreOffice, por favor.", "pad.modals.connected": "Ligado.", - "pad.modals.reconnecting": "A restabelecer ligação ao seu bloco…", + "pad.modals.reconnecting": "A restabelecer ligação à nota…", "pad.modals.forcereconnect": "Forçar restabelecimento de ligação", "pad.modals.reconnecttimer": "A tentar restabelecer ligação", "pad.modals.cancel": "Cancelar", @@ -89,6 +123,8 @@ "pad.modals.deleted.explanation": "Esta nota foi removida.", "pad.modals.rateLimited": "Limitado.", "pad.modals.rateLimited.explanation": "Enviou demasiadas mensagens para este pad, por isso foi desligado.", + "pad.modals.rejected.explanation": "O servidor rejeitou a mensagem que foi enviada pelo teu navegador.", + "pad.modals.rejected.cause": "O server foi atualizado enquanto estávas a ver esta nota, ou talvez seja apenas um bug do Etherpad. Tenta recarregar a página.", "pad.modals.disconnected": "Você foi desligado.", "pad.modals.disconnected.explanation": "A ligação ao servidor foi perdida", "pad.modals.disconnected.cause": "O servidor pode estar indisponível. Por favor, notifique o administrador de serviço se isto continuar a acontecer.", diff --git a/src/node/db/DB.js b/src/node/db/DB.js index 12d3d9f80..c0993e8ec 100644 --- a/src/node/db/DB.js +++ b/src/node/db/DB.js @@ -1,7 +1,7 @@ 'use strict'; /** - * The DB Module provides a database initalized with the settings + * The DB Module provides a database initialized with the settings * provided by the settings module */ @@ -36,7 +36,7 @@ const db = exports.db = null; /** - * Initalizes the database with the settings provided by the settings module + * Initializes the database with the settings provided by the settings module * @param {Function} callback */ exports.init = async () => await new Promise((resolve, reject) => { diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js index 11e8cb1a5..48507949e 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.js @@ -139,7 +139,7 @@ exports.getPad = async (id, text) => { // try to load pad pad = new Pad(id); - // initalize the pad + // initialize the pad await pad.init(text); globalPads.set(id, pad); padList.addPad(id); diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 01981d1f0..2a1f6385b 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -46,7 +46,7 @@ const rateLimiter = new RateLimiterMemory({ }); /** - * A associative array that saves informations about a session + * A associative array that saves information about a session * key = sessionId * values = padId, readonlyPadId, readonly, author, rev * padId = the real padId of the pad @@ -88,7 +88,7 @@ exports.setSocketIO = (socket_io) => { exports.handleConnect = (socket) => { stats.meter('connects').mark(); - // Initalize sessioninfos for this new session + // Initialize sessioninfos for this new session sessioninfos[socket.id] = {}; }; diff --git a/src/node/hooks/express/openapi.js b/src/node/hooks/express/openapi.js index 444077fda..950b56601 100644 --- a/src/node/hooks/express/openapi.js +++ b/src/node/hooks/express/openapi.js @@ -141,7 +141,7 @@ const resources = { // We need an operation that returns a SessionInfo so it can be picked up by the codegen :( info: { operationId: 'getSessionInfo', - summary: 'returns informations about a session', + summary: 'returns information about a session', responseSchema: {info: {$ref: '#/components/schemas/SessionInfo'}}, }, }, diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index 3d9e9debe..58d2f5a44 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -88,7 +88,7 @@ exports.expressCreateServer = (hookName, args, cb) => { // http://stackoverflow.com/questions/23981741/minify-socket-io-socket-io-js-with-1-0 // if(settings.minify) io.enable('browser client minification'); - // Initalize the Socket.IO Router + // Initialize the Socket.IO Router socketIORouter.setSocketIO(io); socketIORouter.addComponent('pad', padMessageHandler); diff --git a/src/node/server.js b/src/node/server.js index afd51b16e..8baf31ff0 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -36,8 +36,8 @@ const wtfnode = require('wtfnode'); * any modules that require newer versions of NodeJS */ const NodeVersion = require('./utils/NodeVersion'); -NodeVersion.enforceMinNodeVersion('10.13.0'); -NodeVersion.checkDeprecationStatus('10.13.0', '1.8.3'); +NodeVersion.enforceMinNodeVersion('10.17.0'); +NodeVersion.checkDeprecationStatus('10.17.0', '1.8.8'); const UpdateCheck = require('./utils/UpdateCheck'); const db = require('./db/DB'); diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index 66a1926a9..49d26ba6b 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -311,7 +311,7 @@ const statFile = (filename, callback, dirStatLimit) => { const lastModifiedDateOfEverything = (callback) => { const folders2check = [`${ROOT_DIR}js/`, `${ROOT_DIR}css/`]; let latestModification = 0; - // go trough this two folders + // go through this two folders async.forEach(folders2check, (path, callback) => { // read the files in the folder fs.readdir(path, (err, files) => { @@ -320,7 +320,7 @@ const lastModifiedDateOfEverything = (callback) => { // we wanna check the directory itself for changes too files.push('.'); - // go trough all files in this folder + // go through all files in this folder async.forEach(files, (filename, callback) => { // get the stat data of this file fs.stat(`${path}/${filename}`, (err, stats) => { diff --git a/src/package.json b/src/package.json index 816f5b587..8399c19bd 100644 --- a/src/package.json +++ b/src/package.json @@ -139,7 +139,7 @@ "root": true }, "engines": { - "node": ">=10.13.0", + "node": "^10.17.0 || >=11.14.0", "npm": ">=5.5.1" }, "repository": { diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js index f70eefc23..254168990 100644 --- a/src/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.js @@ -93,11 +93,8 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool } else if (linestylefilter.ATTRIB_CLASSES[key]) { classes += ` ${linestylefilter.ATTRIB_CLASSES[key]}`; } else { - classes += hooks.callAllStr('aceAttribsToClasses', { - linestylefilter, - key, - value, - }, ' ', ' ', ''); + const results = hooks.callAll('aceAttribsToClasses', {linestylefilter, key, value}); + classes += ` ${results.join(' ')}`; } } } diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 1deee9f7e..753ab7d02 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -294,7 +294,7 @@ const handshake = () => { // set some client vars window.clientVars = obj.data; - // initalize the pad + // initialize the pad pad._afterHandshake(); if (clientVars.readonly) { diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 6243a9305..72da63021 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -1,6 +1,5 @@ -/* global exports, require */ +'use strict'; -const _ = require('underscore'); const pluginDefs = require('./plugin_defs'); // Maps the name of a server-side hook to a string explaining the deprecation @@ -15,66 +14,37 @@ exports.deprecationNotices = {}; const deprecationWarned = {}; -function checkDeprecation(hook) { +const checkDeprecation = (hook) => { const notice = exports.deprecationNotices[hook.hook_name]; if (notice == null) return; if (deprecationWarned[hook.hook_fn_name]) return; console.warn(`${hook.hook_name} hook used by the ${hook.part.plugin} plugin ` + `(${hook.hook_fn_name}) is deprecated: ${notice}`); deprecationWarned[hook.hook_fn_name] = true; -} - -exports.bubbleExceptions = true; - -const hookCallWrapper = function (hook, hook_name, args, cb) { - if (cb === undefined) cb = function (x) { return x; }; - - checkDeprecation(hook); - - // Normalize output to list for both sync and async cases - const normalize = function (x) { - if (x === undefined) return []; - return x; - }; - const normalizedhook = function () { - return normalize(hook.hook_fn(hook_name, args, (x) => cb(normalize(x)))); - }; - - if (exports.bubbleExceptions) { - return normalizedhook(); - } else { - try { - return normalizedhook(); - } catch (ex) { - console.error([hook_name, hook.part.full_name, ex.stack || ex]); - } - } }; -exports.syncMapFirst = function (lst, fn) { - let i; - let result; - for (i = 0; i < lst.length; i++) { - result = fn(lst[i]); - if (result.length) return result; - } - return []; +// Calls the node-style callback when the Promise settles. Unlike util.callbackify, this takes a +// Promise (rather than a function that returns a Promise), and it returns a Promise (rather than a +// function that returns undefined). +const attachCallback = (p, cb) => p.then( + (val) => cb(null, val), + // Callbacks often only check the truthiness, not the nullness, of the first parameter. To avoid + // problems, always pass a truthy value as the first argument if the Promise is rejected. + (err) => cb(err || new Error(err))); + +// Normalizes the value provided by hook functions so that it is always an array. `undefined` (but +// not `null`!) becomes an empty array, array values are returned unmodified, and non-array values +// are wrapped in an array (so `null` becomes `[null]`). +const normalizeValue = (val) => { + // `undefined` is treated the same as `[]`. IMPORTANT: `null` is *not* treated the same as `[]` + // because some hooks use `null` as a special value. + if (val === undefined) return []; + if (Array.isArray(val)) return val; + return [val]; }; -exports.mapFirst = function (lst, fn, cb, predicate) { - if (predicate == null) predicate = (x) => (x != null && x.length > 0); - let i = 0; - - var next = function () { - if (i >= lst.length) return cb(null, []); - fn(lst[i++], (err, result) => { - if (err) return cb(err); - if (predicate(result)) return cb(null, result); - next(); - }); - }; - next(); -}; +// Flattens the array one level. +const flatten1 = (array) => array.reduce((a, b) => a.concat(b), []); // Calls the hook function synchronously and returns the value provided by the hook function (via // callback or return value). @@ -104,7 +74,7 @@ exports.mapFirst = function (lst, fn, cb, predicate) { // // See the tests in tests/backend/specs/hooks.js for examples of supported and prohibited behaviors. // -function callHookFnSync(hook, context) { +const callHookFnSync = (hook, context) => { checkDeprecation(hook); // This var is used to keep track of whether the hook function already settled. @@ -177,21 +147,36 @@ function callHookFnSync(hook, context) { // The hook function is assumed to not have a callback parameter, so fall through and accept // `undefined` as the resolved value. // - // IMPORTANT: "Rest" parameters and default parameters are not counted in`Function.length`, so - // the assumption does not hold for wrappers like `(...args) => { real(...args); }`. Such - // functions will still work properly without any logged warnings or errors for now, but: + // IMPORTANT: "Rest" parameters and default parameters are not included in `Function.length`, + // so the assumption does not hold for wrappers such as: + // + // const wrapper = (...args) => real(...args); + // + // ECMAScript does not provide a way to determine whether a function has default or rest + // parameters, so there is no way to be certain that a hook function with `length` < 3 will + // not call the callback. Synchronous hook functions that call the callback even though + // `length` < 3 will still work properly without any logged warnings or errors, but: + // // * Once the hook is upgraded to support asynchronous hook functions, calling the callback - // will (eventually) cause a double settle error, and the function might prematurely + // asynchronously will cause a double settle error, and the hook function will prematurely // resolve to `undefined` instead of the desired value. + // // * The above "unsettled function" warning is not logged if the function fails to call the // callback like it is supposed to. + // + // Wrapper functions can avoid problems by setting the wrapper's `length` property to match + // the real function's `length` property: + // + // Object.defineProperty(wrapper, 'length', {value: real.length}); } } settle(null, val, 'returned value'); return outcome.val; -} +}; +// DEPRECATED: Use `callAllSerial()` or `aCallAll()` instead. +// // Invokes all registered hook functions synchronously. // // Arguments: @@ -203,15 +188,10 @@ function callHookFnSync(hook, context) { // 1. Collect all values returned by the hook functions into an array. // 2. Convert each `undefined` entry into `[]`. // 3. Flatten one level. -exports.callAll = function (hookName, context) { +exports.callAll = (hookName, context) => { if (context == null) context = {}; const hooks = pluginDefs.hooks[hookName] || []; - return _.flatten(hooks.map((hook) => { - const ret = callHookFnSync(hook, context); - // `undefined` (but not `null`!) is treated the same as []. - if (ret === undefined) return []; - return ret; - }), 1); + return flatten1(hooks.map((hook) => normalizeValue(callHookFnSync(hook, context)))); }; // Calls the hook function asynchronously and returns a Promise that either resolves to the hook @@ -248,7 +228,7 @@ exports.callAll = function (hookName, context) { // // See the tests in tests/backend/specs/hooks.js for examples of supported and prohibited behaviors. // -async function callHookFnAsync(hook, context) { +const callHookFnAsync = async (hook, context) => { checkDeprecation(hook); return await new Promise((resolve, reject) => { // This var is used to keep track of whether the hook function already settled. @@ -312,10 +292,21 @@ async function callHookFnAsync(hook, context) { // The hook function is assumed to not have a callback parameter, so fall through and accept // `undefined` as the resolved value. // - // IMPORTANT: "Rest" parameters and default parameters are not counted in `Function.length`, - // so the assumption does not hold for wrappers like `(...args) => { real(...args); }`. For - // such functions, calling the callback will (eventually) cause a double settle error, and - // the function might prematurely resolve to `undefined` instead of the desired value. + // IMPORTANT: "Rest" parameters and default parameters are not included in + // `Function.length`, so the assumption does not hold for wrappers such as: + // + // const wrapper = (...args) => real(...args); + // + // ECMAScript does not provide a way to determine whether a function has default or rest + // parameters, so there is no way to be certain that a hook function with `length` < 3 will + // not call the callback. Hook functions with `length` < 3 that call the callback + // asynchronously will cause a double settle error, and the hook function will prematurely + // resolve to `undefined` instead of the desired value. + // + // Wrapper functions can avoid problems by setting the wrapper's `length` property to match + // the real function's `length` property: + // + // Object.defineProperty(wrapper, 'length', {value: real.length}); } } @@ -326,17 +317,21 @@ async function callHookFnAsync(hook, context) { (val) => settle(null, val, 'returned value'), (err) => settle(err, null, 'Promise rejection')); }); -} +}; -// Invokes all registered hook functions asynchronously. +// Invokes all registered hook functions asynchronously and concurrently. This is NOT the async +// equivalent of `callAll()`: `callAll()` calls the hook functions serially (one at a time) but this +// function calls them concurrently. Use `callAllSerial()` if the hook functions must be called one +// at a time. // // Arguments: // * hookName: Name of the hook to invoke. // * context: Passed unmodified to the hook functions, except nullish becomes {}. -// * cb: Deprecated callback. The following: +// * cb: Deprecated. Optional node-style callback. The following: // const p1 = hooks.aCallAll('myHook', context, cb); // is equivalent to: -// const p2 = hooks.aCallAll('myHook', context).then((val) => cb(null, val), cb); +// const p2 = hooks.aCallAll('myHook', context).then( +// (val) => cb(null, val), (err) => cb(err || new Error(err))); // // Return value: // If cb is nullish, this function resolves to a flattened array of hook results. Specifically, it @@ -345,57 +340,75 @@ async function callHookFnAsync(hook, context) { // 2. Convert each `undefined` entry into `[]`. // 3. Flatten one level. // If cb is non-null, this function resolves to the value returned by cb. -exports.aCallAll = async (hookName, context, cb) => { +exports.aCallAll = async (hookName, context, cb = null) => { + if (cb != null) return await attachCallback(exports.aCallAll(hookName, context), cb); if (context == null) context = {}; const hooks = pluginDefs.hooks[hookName] || []; - let resultsPromise = Promise.all(hooks.map((hook) => callHookFnAsync(hook, context) - // `undefined` (but not `null`!) is treated the same as []. - .then((result) => (result === undefined) ? [] : result))).then((results) => _.flatten(results, 1)); - if (cb != null) resultsPromise = resultsPromise.then((val) => cb(null, val), cb); - return await resultsPromise; + const results = await Promise.all( + hooks.map(async (hook) => normalizeValue(await callHookFnAsync(hook, context)))); + return flatten1(results); }; -exports.callFirst = function (hook_name, args) { - if (!args) args = {}; - if (pluginDefs.hooks[hook_name] === undefined) return []; - return exports.syncMapFirst(pluginDefs.hooks[hook_name], (hook) => hookCallWrapper(hook, hook_name, args)); -}; - -function aCallFirst(hook_name, args, cb, predicate) { - if (!args) args = {}; - if (!cb) cb = function () {}; - if (pluginDefs.hooks[hook_name] === undefined) return cb(null, []); - exports.mapFirst( - pluginDefs.hooks[hook_name], - (hook, cb) => { - hookCallWrapper(hook, hook_name, args, (res) => { cb(null, res); }); - }, - cb, - predicate - ); -} - -/* return a Promise if cb is not supplied */ -exports.aCallFirst = function (hook_name, args, cb, predicate) { - if (cb === undefined) { - return new Promise((resolve, reject) => { - aCallFirst(hook_name, args, (err, res) => err ? reject(err) : resolve(res), predicate); - }); - } else { - return aCallFirst(hook_name, args, cb, predicate); +// Like `aCallAll()` except the hook functions are called one at a time instead of concurrently. +// Only use this function if the hook functions must be called one at a time, otherwise use +// `aCallAll()`. +exports.callAllSerial = async (hookName, context) => { + if (context == null) context = {}; + const hooks = pluginDefs.hooks[hookName] || []; + const results = []; + for (const hook of hooks) { + results.push(normalizeValue(await callHookFnAsync(hook, context))); } + return flatten1(results); }; -exports.callAllStr = function (hook_name, args, sep, pre, post) { - if (sep == undefined) sep = ''; - if (pre == undefined) pre = ''; - if (post == undefined) post = ''; - const newCallhooks = []; - const callhooks = exports.callAll(hook_name, args); - for (let i = 0, ii = callhooks.length; i < ii; i++) { - newCallhooks[i] = pre + callhooks[i] + post; +// DEPRECATED: Use `aCallFirst()` instead. +// +// Like `aCallFirst()`, but synchronous. Hook functions must provide their values synchronously. +exports.callFirst = (hookName, context) => { + if (context == null) context = {}; + const predicate = (val) => val.length; + const hooks = pluginDefs.hooks[hookName] || []; + for (const hook of hooks) { + const val = normalizeValue(callHookFnSync(hook, context)); + if (predicate(val)) return val; } - return newCallhooks.join(sep || ''); + return []; +}; + +// Invokes the registered hook functions one at a time until one provides a value that meets a +// customizable condition. +// +// Arguments: +// * hookName: Name of the hook to invoke. +// * context: Passed unmodified to the hook functions, except nullish becomes {}. +// * cb: Deprecated callback. The following: +// const p1 = hooks.aCallFirst('myHook', context, cb); +// is equivalent to: +// const p2 = hooks.aCallFirst('myHook', context).then( +// (val) => cb(null, val), (err) => cb(err || new Error(err))); +// * predicate: Optional predicate function that returns true if the hook function provided a +// value that satisfies a desired condition. If nullish, the predicate defaults to a non-empty +// array check. The predicate is invoked each time a hook function returns. It takes one +// argument: the normalized value provided by the hook function. If the predicate returns +// truthy, iteration over the hook functions stops (no more hook functions will be called). +// +// Return value: +// If cb is nullish, resolves to an array that is either the normalized value that satisfied the +// predicate or empty if the predicate was never satisfied. If cb is non-nullish, resolves to the +// value returned from cb(). +exports.aCallFirst = async (hookName, context, cb = null, predicate = null) => { + if (cb != null) { + return await attachCallback(exports.aCallFirst(hookName, context, null, predicate), cb); + } + if (context == null) context = {}; + if (predicate == null) predicate = (val) => val.length; + const hooks = pluginDefs.hooks[hookName] || []; + for (const hook of hooks) { + const val = normalizeValue(await callHookFnAsync(hook, context)); + if (predicate(val)) return val; + } + return []; }; exports.exportedForTestingOnly = { diff --git a/src/static/js/pluginfw/installer.js b/src/static/js/pluginfw/installer.js index 7d29b91b1..ae5c06e10 100644 --- a/src/static/js/pluginfw/installer.js +++ b/src/static/js/pluginfw/installer.js @@ -1,6 +1,8 @@ +'use strict'; + const log4js = require('log4js'); -const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); -const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); +const plugins = require('./plugins'); +const hooks = require('./hooks'); const npm = require('npm'); const request = require('request'); const util = require('util'); @@ -13,22 +15,22 @@ const loadNpm = async () => { npm.on('log', log4js.getLogger('npm').log); }; +const onAllTasksFinished = () => { + hooks.aCallAll('restartServer', {}, () => {}); +}; + let tasks = 0; function wrapTaskCb(cb) { tasks++; - return function () { - cb && cb.apply(this, arguments); + return function (...args) { + cb && cb.apply(this, args); tasks--; - if (tasks == 0) onAllTasksFinished(); + if (tasks === 0) onAllTasksFinished(); }; } -function onAllTasksFinished() { - hooks.aCallAll('restartServer', {}, () => {}); -} - exports.uninstall = async (pluginName, cb = null) => { cb = wrapTaskCb(cb); try { @@ -60,7 +62,7 @@ exports.install = async (pluginName, cb = null) => { exports.availablePlugins = null; let cacheTimestamp = 0; -exports.getAvailablePlugins = function (maxCacheAge) { +exports.getAvailablePlugins = (maxCacheAge) => { const nowTimestamp = Math.round(Date.now() / 1000); return new Promise((resolve, reject) => { @@ -87,31 +89,33 @@ exports.getAvailablePlugins = function (maxCacheAge) { }; -exports.search = function (searchTerm, maxCacheAge) { - return exports.getAvailablePlugins(maxCacheAge).then((results) => { - const res = {}; +exports.search = (searchTerm, maxCacheAge) => exports.getAvailablePlugins(maxCacheAge).then( + (results) => { + const res = {}; - if (searchTerm) { - searchTerm = searchTerm.toLowerCase(); - } - - for (const pluginName in results) { - // for every available plugin - if (pluginName.indexOf(plugins.prefix) != 0) continue; // TODO: Also search in keywords here! - - if (searchTerm && !~results[pluginName].name.toLowerCase().indexOf(searchTerm) && - (typeof results[pluginName].description !== 'undefined' && !~results[pluginName].description.toLowerCase().indexOf(searchTerm)) - ) { - if (typeof results[pluginName].description === 'undefined') { - console.debug('plugin without Description: %s', results[pluginName].name); - } - - continue; + if (searchTerm) { + searchTerm = searchTerm.toLowerCase(); } - res[pluginName] = results[pluginName]; - } + for (const pluginName in results) { + // for every available plugin + // TODO: Also search in keywords here! + if (pluginName.indexOf(plugins.prefix) !== 0) continue; - return res; - }); -}; + if (searchTerm && !~results[pluginName].name.toLowerCase().indexOf(searchTerm) && + (typeof results[pluginName].description !== 'undefined' && + !~results[pluginName].description.toLowerCase().indexOf(searchTerm)) + ) { + if (typeof results[pluginName].description === 'undefined') { + console.debug('plugin without Description: %s', results[pluginName].name); + } + + continue; + } + + res[pluginName] = results[pluginName]; + } + + return res; + } +); diff --git a/src/static/js/pluginfw/plugin_defs.js b/src/static/js/pluginfw/plugin_defs.js index 95bbcb95c..768d99c3e 100644 --- a/src/static/js/pluginfw/plugin_defs.js +++ b/src/static/js/pluginfw/plugin_defs.js @@ -1,3 +1,5 @@ +'use strict'; + // This module contains processed plugin definitions. The data structures in this file are set by // plugins.js (server) or client_plugins.js (client). diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js index 52fbdd271..4acdee7bd 100644 --- a/src/static/js/pluginfw/plugins.js +++ b/src/static/js/pluginfw/plugins.js @@ -1,6 +1,7 @@ +'use strict'; + const fs = require('fs').promises; const hooks = require('./hooks'); -const npm = require('npm/lib/npm.js'); const readInstalled = require('./read-installed.js'); const path = require('path'); const tsort = require('./tsort'); @@ -13,11 +14,9 @@ const defs = require('./plugin_defs'); exports.prefix = 'ep_'; -exports.formatPlugins = function () { - return _.keys(defs.plugins).join(', '); -}; +exports.formatPlugins = () => Object.keys(defs.plugins).join(', '); -exports.formatPluginsWithVersion = function () { +exports.formatPluginsWithVersion = () => { const plugins = []; _.forEach(defs.plugins, (plugin) => { if (plugin.package.name !== 'ep_etherpad-lite') { @@ -28,17 +27,16 @@ exports.formatPluginsWithVersion = function () { return plugins.join(', '); }; -exports.formatParts = function () { - return _.map(defs.parts, (part) => part.full_name).join('\n'); -}; +exports.formatParts = () => _.map(defs.parts, (part) => part.full_name).join('\n'); -exports.formatHooks = function (hook_set_name) { +exports.formatHooks = (hook_set_name) => { const res = []; const hooks = pluginUtils.extractHooks(defs.parts, hook_set_name || 'hooks'); _.chain(hooks).keys().forEach((hook_name) => { _.forEach(hooks[hook_name], (hook) => { - res.push(`
${hook.hook_name}
${hook.hook_fn_name} from ${hook.part.full_name}
`); + res.push(`
${hook.hook_name}
${hook.hook_fn_name} ` + + `from ${hook.part.full_name}
`); }); }); return `
${res.join('\n')}
`; @@ -57,7 +55,7 @@ const callInit = async () => { })); }; -exports.pathNormalization = function (part, hook_fn_name, hook_name) { +exports.pathNormalization = (part, hook_fn_name, hook_name) => { const tmp = hook_fn_name.split(':'); // hook_fn_name might be something like 'C:\\foo.js:myFunc'. // If there is a single colon assume it's 'filename:funcname' not 'C:\\filename'. const functionName = (tmp.length > 1 ? tmp.pop() : null) || hook_name; @@ -67,7 +65,7 @@ exports.pathNormalization = function (part, hook_fn_name, hook_name) { return `${fileName}:${functionName}`; }; -exports.update = async function () { +exports.update = async () => { const packages = await exports.getPackages(); const parts = {}; // Key is full name. sortParts converts this into a topologically sorted array. const plugins = {}; @@ -83,13 +81,14 @@ exports.update = async function () { await callInit(); }; -exports.getPackages = async function () { - // Load list of installed NPM packages, flatten it to a list, and filter out only packages with names that +exports.getPackages = async () => { + // Load list of installed NPM packages, flatten it to a list, + // and filter out only packages with names that const dir = settings.root; const data = await util.promisify(readInstalled)(dir); const packages = {}; - function flatten(deps) { + const flatten = (deps) => { _.chain(deps).keys().each((name) => { if (name.indexOf(exports.prefix) === 0) { packages[name] = _.clone(deps[name]); @@ -102,7 +101,7 @@ exports.getPackages = async function () { // I don't think we need recursion // if (deps[name].dependencies !== undefined) flatten(deps[name].dependencies); }); - } + }; const tmp = {}; tmp[data.name] = data; @@ -110,7 +109,7 @@ exports.getPackages = async function () { return packages; }; -async function loadPlugin(packages, plugin_name, plugins, parts) { +const loadPlugin = async (packages, plugin_name, plugins, parts) => { const plugin_path = path.resolve(packages[plugin_name].path, 'ep.json'); try { const data = await fs.readFile(plugin_path); @@ -129,9 +128,9 @@ async function loadPlugin(packages, plugin_name, plugins, parts) { } catch (er) { console.error(`Unable to load plugin definition file ${plugin_path}`); } -} +}; -function partsToParentChildList(parts) { +const partsToParentChildList = (parts) => { const res = []; _.chain(parts).keys().forEach((name) => { _.each(parts[name].post || [], (child_name) => { @@ -145,15 +144,9 @@ function partsToParentChildList(parts) { } }); return res; -} +}; // Used only in Node, so no need for _ -function sortParts(parts) { - return tsort( - partsToParentChildList(parts) - ).filter( - (name) => parts[name] !== undefined - ).map( - (name) => parts[name] - ); -} +const sortParts = (parts) => tsort(partsToParentChildList(parts)) + .filter((name) => parts[name] !== undefined) + .map((name) => parts[name]); diff --git a/src/static/js/pluginfw/shared.js b/src/static/js/pluginfw/shared.js index 749706812..981cd2558 100644 --- a/src/static/js/pluginfw/shared.js +++ b/src/static/js/pluginfw/shared.js @@ -1,3 +1,4 @@ +'use strict'; const _ = require('underscore'); const defs = require('./plugin_defs'); @@ -8,13 +9,13 @@ const disabledHookReasons = { }, }; -function loadFn(path, hookName) { +const loadFn = (path, hookName) => { let functionName; const parts = path.split(':'); // on windows: C:\foo\bar:xyz - if (parts[0].length == 1) { - if (parts.length == 3) { + if (parts[0].length === 1) { + if (parts.length === 3) { functionName = parts.pop(); } path = parts.join(':'); @@ -30,9 +31,9 @@ function loadFn(path, hookName) { fn = fn[name]; }); return fn; -} +}; -function extractHooks(parts, hook_set_name, normalizer) { +const extractHooks = (parts, hook_set_name, normalizer) => { const hooks = {}; _.each(parts, (part) => { _.chain(part[hook_set_name] || {}) @@ -50,20 +51,23 @@ function extractHooks(parts, hook_set_name, normalizer) { const disabledReason = (disabledHookReasons[hook_set_name] || {})[hook_name]; if (disabledReason) { - console.error(`Hook ${hook_set_name}/${hook_name} is disabled. Reason: ${disabledReason}`); + console.error( + `Hook ${hook_set_name}/${hook_name} is disabled. Reason: ${disabledReason}`); console.error(`The hook function ${hook_fn_name} from plugin ${part.plugin} ` + - 'will never be called, which may cause the plugin to fail'); - console.error(`Please update the ${part.plugin} plugin to not use the ${hook_name} hook`); + 'will never be called, which may cause the plugin to fail'); + console.error( + `Please update the ${part.plugin} plugin to not use the ${hook_name} hook`); return; } - + let hook_fn; try { - var hook_fn = loadFn(hook_fn_name, hook_name); + hook_fn = loadFn(hook_fn_name, hook_name); if (!hook_fn) { - throw 'Not a function'; + throw new Error('Not a function'); } } catch (exc) { - console.error(`Failed to load '${hook_fn_name}' for '${part.full_name}/${hook_set_name}/${hook_name}': ${exc.toString()}`); + console.error(`Failed to load '${hook_fn_name}' for ` + + `'${part.full_name}/${hook_set_name}/${hook_name}': ${exc.toString()}`); } if (hook_fn) { if (hooks[hook_name] == null) hooks[hook_name] = []; @@ -72,7 +76,7 @@ function extractHooks(parts, hook_set_name, normalizer) { }); }); return hooks; -} +}; exports.extractHooks = extractHooks; @@ -88,10 +92,10 @@ exports.extractHooks = extractHooks; * No plugins: [] * Some plugins: [ 'ep_adminpads', 'ep_add_buttons', 'ep_activepads' ] */ -exports.clientPluginNames = function () { +exports.clientPluginNames = () => { const client_plugin_names = _.uniq( defs.parts - .filter((part) => part.hasOwnProperty('client_hooks')) + .filter((part) => Object.prototype.hasOwnProperty.call(part, 'client_hooks')) .map((part) => `plugin-${part.plugin}`) ); diff --git a/tests/backend/fuzzImportTest.js b/tests/backend/fuzzImportTest.js index e2667e8f9..eb5a17d17 100644 --- a/tests/backend/fuzzImportTest.js +++ b/tests/backend/fuzzImportTest.js @@ -42,7 +42,7 @@ function runTest(number) { let fN = '/test.txt'; let cT = 'text/plain'; - // To be more agressive every other test we mess with Etherpad + // To be more aggressive every other test we mess with Etherpad // We provide a weird file name and also set a weird contentType if (number % 2 == 0) { fN = froth().toString(); diff --git a/tests/backend/specs/api/importexport.js b/tests/backend/specs/api/importexport.js index 541dd822e..794a702bb 100644 --- a/tests/backend/specs/api/importexport.js +++ b/tests/backend/specs/api/importexport.js @@ -94,7 +94,7 @@ const testImports = { wantText: ' word1 word2 word3\n\n', }, 'nonBreakingSpacePreceededBySpaceBetweenWords': { - description: 'A non-breaking space preceeded by a normal space', + description: 'A non-breaking space preceded by a normal space', input: '  word1  word2  word3
', wantHTML: ' word1  word2  word3

', wantText: ' word1 word2 word3\n\n', @@ -191,7 +191,7 @@ const testImports = { wantText: 'а б в г ґ д е є ж з и і ї й к л м н о\nmultiple\n lines\n in\n pre\n\nп р с т у ф х ц ч ш щ ю я ь\n\n', }, 'preIntroducesASpace': { - description: 'pre should be on a new line not preceeded by a space', + description: 'pre should be on a new line not preceded by a space', input: `

1

preline
diff --git a/tests/backend/specs/api/pad.js b/tests/backend/specs/api/pad.js
index cf198edcb..004469bfb 100644
--- a/tests/backend/specs/api/pad.js
+++ b/tests/backend/specs/api/pad.js
@@ -107,9 +107,9 @@ describe(__filename, function () {
                          -> setText(padId, "hello world")
                           -> getLastEdited(padID) -- Should be when pad was made
                            -> getText(padId) -- Should be "hello world"
-                            -> movePad(padID, newPadId) -- Should provide consistant pad data
+                            -> movePad(padID, newPadId) -- Should provide consistent pad data
                              -> getText(newPadId) -- Should be "hello world"
-                              -> movePad(newPadID, originalPadId) -- Should provide consistant pad data
+                              -> movePad(newPadID, originalPadId) -- Should provide consistent pad data
                                -> getText(originalPadId) -- Should be "hello world"
                                 -> getLastEdited(padID) -- Should not be 0
                                 -> appendText(padID, "hello")
diff --git a/tests/backend/specs/api/sessionsAndGroups.js b/tests/backend/specs/api/sessionsAndGroups.js
index bb3e28719..239a079f1 100644
--- a/tests/backend/specs/api/sessionsAndGroups.js
+++ b/tests/backend/specs/api/sessionsAndGroups.js
@@ -100,6 +100,83 @@ describe(__filename, function () {
             assert(res.body.data.groupID);
           });
     });
+
+    // Test coverage for https://github.com/ether/etherpad-lite/issues/4227
+    // Creates a group, creates 2 sessions, 2 pads and then deletes the group.
+    it('createGroup', async function () {
+      await api.get(endPoint('createGroup'))
+          .expect(200)
+          .expect('Content-Type', /json/)
+          .expect((res) => {
+            assert.equal(res.body.code, 0);
+            assert(res.body.data.groupID);
+            groupID = res.body.data.groupID;
+          });
+    });
+
+    it('createAuthor', async function () {
+      await api.get(endPoint('createAuthor'))
+          .expect(200)
+          .expect('Content-Type', /json/)
+          .expect((res) => {
+            assert.equal(res.body.code, 0);
+            assert(res.body.data.authorID);
+            authorID = res.body.data.authorID;
+          });
+    });
+
+    it('createSession', async function () {
+      await api.get(`${endPoint('createSession')
+      }&authorID=${authorID}&groupID=${groupID}&validUntil=999999999999`)
+          .expect(200)
+          .expect('Content-Type', /json/)
+          .expect((res) => {
+            assert.equal(res.body.code, 0);
+            assert(res.body.data.sessionID);
+            sessionID = res.body.data.sessionID;
+          });
+    });
+
+    it('createSession', async function () {
+      await api.get(`${endPoint('createSession')
+      }&authorID=${authorID}&groupID=${groupID}&validUntil=999999999999`)
+          .expect(200)
+          .expect('Content-Type', /json/)
+          .expect((res) => {
+            assert.equal(res.body.code, 0);
+            assert(res.body.data.sessionID);
+            sessionID = res.body.data.sessionID;
+          });
+    });
+
+    it('createGroupPad', async function () {
+      await api.get(`${endPoint('createGroupPad')}&groupID=${groupID}&padName=x1234567`)
+          .expect(200)
+          .expect('Content-Type', /json/)
+          .expect((res) => {
+            assert.equal(res.body.code, 0);
+          });
+    });
+
+    it('createGroupPad', async function () {
+      await api.get(`${endPoint('createGroupPad')}&groupID=${groupID}&padName=x12345678`)
+          .expect(200)
+          .expect('Content-Type', /json/)
+          .expect((res) => {
+            assert.equal(res.body.code, 0);
+          });
+    });
+
+    it('deleteGroup', async function () {
+      await api.get(`${endPoint('deleteGroup')}&groupID=${groupID}`)
+          .expect(200)
+          .expect('Content-Type', /json/)
+          .expect((res) => {
+            assert.equal(res.body.code, 0);
+          });
+    });
+    // End of coverage for https://github.com/ether/etherpad-lite/issues/4227
+
   });
 
   describe('API: Author creation', function () {
diff --git a/tests/backend/specs/contentcollector.js b/tests/backend/specs/contentcollector.js
index 13a1cf116..22d18380c 100644
--- a/tests/backend/specs/contentcollector.js
+++ b/tests/backend/specs/contentcollector.js
@@ -140,7 +140,7 @@ const tests = {
     wantText: ['  word1  word2   word3'],
   },
   nonBreakingSpacePreceededBySpaceBetweenWords: {
-    description: 'A non-breaking space preceeded by a normal space',
+    description: 'A non-breaking space preceded by a normal space',
     html: '  word1  word2  word3
', wantLineAttribs: ['+l'], wantText: [' word1 word2 word3'], @@ -240,7 +240,7 @@ pre ], }, preIntroducesASpace: { - description: 'pre should be on a new line not preceeded by a space', + description: 'pre should be on a new line not preceded by a space', html: `

1

preline
diff --git a/tests/backend/specs/hooks.js b/tests/backend/specs/hooks.js
index 21fb41bb5..77ac8a57c 100644
--- a/tests/backend/specs/hooks.js
+++ b/tests/backend/specs/hooks.js
@@ -1,11 +1,8 @@
 'use strict';
-
-function m(mod) { return `${__dirname}/../../../src/${mod}`; }
-
 const assert = require('assert').strict;
-const hooks = require(m('static/js/pluginfw/hooks'));
-const plugins = require(m('static/js/pluginfw/plugin_defs'));
-const sinon = require(m('node_modules/sinon'));
+const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
+const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs');
+const sinon = require('ep_etherpad-lite/node_modules/sinon');
 
 describe(__filename, function () {
   const hookName = 'testHook';
@@ -209,7 +206,7 @@ describe(__filename, function () {
     // Test various ways a hook might attempt to settle twice. (Examples: call the callback a second
     // time, or call the callback and then return a value.)
     describe('bad hook function behavior (double settle)', function () {
-      beforeEach(function () {
+      beforeEach(async function () {
         sinon.stub(console, 'error');
       });
 
@@ -413,6 +410,90 @@ describe(__filename, function () {
     });
   });
 
+  describe('hooks.callFirst', function () {
+    it('no registered hooks (undefined) -> []', async function () {
+      delete plugins.hooks.testHook;
+      assert.deepEqual(hooks.callFirst(hookName), []);
+    });
+
+    it('no registered hooks (empty list) -> []', async function () {
+      testHooks.length = 0;
+      assert.deepEqual(hooks.callFirst(hookName), []);
+    });
+
+    it('passes hook name => {}', async function () {
+      hook.hook_fn = (hn) => { assert.equal(hn, hookName); };
+      hooks.callFirst(hookName);
+    });
+
+    it('undefined context => {}', async function () {
+      hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); };
+      hooks.callFirst(hookName);
+    });
+
+    it('null context => {}', async function () {
+      hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); };
+      hooks.callFirst(hookName, null);
+    });
+
+    it('context unmodified', async function () {
+      const wantContext = {};
+      hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); };
+      hooks.callFirst(hookName, wantContext);
+    });
+
+    it('predicate never satisfied -> calls all in order', async function () {
+      const gotCalls = [];
+      testHooks.length = 0;
+      for (let i = 0; i < 3; i++) {
+        const hook = makeHook();
+        hook.hook_fn = () => { gotCalls.push(i); };
+        testHooks.push(hook);
+      }
+      assert.deepEqual(hooks.callFirst(hookName), []);
+      assert.deepEqual(gotCalls, [0, 1, 2]);
+    });
+
+    it('stops when predicate is satisfied', async function () {
+      testHooks.length = 0;
+      testHooks.push(makeHook(), makeHook('val1'), makeHook('val2'));
+      assert.deepEqual(hooks.callFirst(hookName), ['val1']);
+    });
+
+    it('skips values that do not satisfy predicate (undefined)', async function () {
+      testHooks.length = 0;
+      testHooks.push(makeHook(), makeHook('val1'));
+      assert.deepEqual(hooks.callFirst(hookName), ['val1']);
+    });
+
+    it('skips values that do not satisfy predicate (empty list)', async function () {
+      testHooks.length = 0;
+      testHooks.push(makeHook([]), makeHook('val1'));
+      assert.deepEqual(hooks.callFirst(hookName), ['val1']);
+    });
+
+    it('null satisifes the predicate', async function () {
+      testHooks.length = 0;
+      testHooks.push(makeHook(null), makeHook('val1'));
+      assert.deepEqual(hooks.callFirst(hookName), [null]);
+    });
+
+    it('non-empty arrays are returned unmodified', async function () {
+      const want = ['val1'];
+      testHooks.length = 0;
+      testHooks.push(makeHook(want), makeHook(['val2']));
+      assert.equal(hooks.callFirst(hookName), want); // Note: *NOT* deepEqual!
+    });
+
+    it('value can be passed via callback', async function () {
+      const want = {};
+      hook.hook_fn = (hn, ctx, cb) => { cb(want); };
+      const got = hooks.callFirst(hookName);
+      assert.deepEqual(got, [want]);
+      assert.equal(got[0], want); // Note: *NOT* deepEqual!
+    });
+  });
+
   describe('callHookFnAsync', function () {
     const callHookFnAsync = hooks.exportedForTestingOnly.callHookFnAsync; // Convenience shorthand.
 
@@ -587,7 +668,7 @@ describe(__filename, function () {
     // Test various ways a hook might attempt to settle twice. (Examples: call the callback a second
     // time, or call the callback and then return a value.)
     describe('bad hook function behavior (double settle)', function () {
-      beforeEach(function () {
+      beforeEach(async function () {
         sinon.stub(console, 'error');
       });
 
@@ -935,4 +1016,243 @@ describe(__filename, function () {
       });
     });
   });
+
+  describe('hooks.callAllSerial', function () {
+    describe('basic behavior', function () {
+      it('calls all asynchronously, serially, in order', async function () {
+        const gotCalls = [];
+        testHooks.length = 0;
+        for (let i = 0; i < 3; i++) {
+          const hook = makeHook();
+          hook.hook_fn = async () => {
+            gotCalls.push(i);
+            // Check gotCalls asynchronously to ensure that the next hook function does not start
+            // executing before this hook function has resolved.
+            return await new Promise((resolve) => {
+              setImmediate(() => {
+                assert.deepEqual(gotCalls, [...Array(i + 1).keys()]);
+                resolve(i);
+              });
+            });
+          };
+          testHooks.push(hook);
+        }
+        assert.deepEqual(await hooks.callAllSerial(hookName), [0, 1, 2]);
+        assert.deepEqual(gotCalls, [0, 1, 2]);
+      });
+
+      it('passes hook name', async function () {
+        hook.hook_fn = async (hn) => { assert.equal(hn, hookName); };
+        await hooks.callAllSerial(hookName);
+      });
+
+      it('undefined context -> {}', async function () {
+        hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); };
+        await hooks.callAllSerial(hookName);
+      });
+
+      it('null context -> {}', async function () {
+        hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); };
+        await hooks.callAllSerial(hookName, null);
+      });
+
+      it('context unmodified', async function () {
+        const wantContext = {};
+        hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); };
+        await hooks.callAllSerial(hookName, wantContext);
+      });
+    });
+
+    describe('result processing', function () {
+      it('no registered hooks (undefined) -> []', async function () {
+        delete plugins.hooks[hookName];
+        assert.deepEqual(await hooks.callAllSerial(hookName), []);
+      });
+
+      it('no registered hooks (empty list) -> []', async function () {
+        testHooks.length = 0;
+        assert.deepEqual(await hooks.callAllSerial(hookName), []);
+      });
+
+      it('flattens one level', async function () {
+        testHooks.length = 0;
+        testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]]));
+        assert.deepEqual(await hooks.callAllSerial(hookName), [1, 2, [3]]);
+      });
+
+      it('filters out undefined', async function () {
+        testHooks.length = 0;
+        testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]), makeHook(Promise.resolve()));
+        assert.deepEqual(await hooks.callAllSerial(hookName), [2, [3]]);
+      });
+
+      it('preserves null', async function () {
+        testHooks.length = 0;
+        testHooks.push(makeHook(null), makeHook([2]), makeHook(Promise.resolve(null)));
+        assert.deepEqual(await hooks.callAllSerial(hookName), [null, 2, null]);
+      });
+
+      it('all undefined -> []', async function () {
+        testHooks.length = 0;
+        testHooks.push(makeHook(), makeHook(Promise.resolve()));
+        assert.deepEqual(await hooks.callAllSerial(hookName), []);
+      });
+    });
+  });
+
+  describe('hooks.aCallFirst', function () {
+    it('no registered hooks (undefined) -> []', async function () {
+      delete plugins.hooks.testHook;
+      assert.deepEqual(await hooks.aCallFirst(hookName), []);
+    });
+
+    it('no registered hooks (empty list) -> []', async function () {
+      testHooks.length = 0;
+      assert.deepEqual(await hooks.aCallFirst(hookName), []);
+    });
+
+    it('passes hook name => {}', async function () {
+      hook.hook_fn = (hn) => { assert.equal(hn, hookName); };
+      await hooks.aCallFirst(hookName);
+    });
+
+    it('undefined context => {}', async function () {
+      hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); };
+      await hooks.aCallFirst(hookName);
+    });
+
+    it('null context => {}', async function () {
+      hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); };
+      await hooks.aCallFirst(hookName, null);
+    });
+
+    it('context unmodified', async function () {
+      const wantContext = {};
+      hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); };
+      await hooks.aCallFirst(hookName, wantContext);
+    });
+
+    it('default predicate: predicate never satisfied -> calls all in order', async function () {
+      const gotCalls = [];
+      testHooks.length = 0;
+      for (let i = 0; i < 3; i++) {
+        const hook = makeHook();
+        hook.hook_fn = () => { gotCalls.push(i); };
+        testHooks.push(hook);
+      }
+      assert.deepEqual(await hooks.aCallFirst(hookName), []);
+      assert.deepEqual(gotCalls, [0, 1, 2]);
+    });
+
+    it('calls hook functions serially', async function () {
+      const gotCalls = [];
+      testHooks.length = 0;
+      for (let i = 0; i < 3; i++) {
+        const hook = makeHook();
+        hook.hook_fn = async () => {
+          gotCalls.push(i);
+          // Check gotCalls asynchronously to ensure that the next hook function does not start
+          // executing before this hook function has resolved.
+          return await new Promise((resolve) => {
+            setImmediate(() => {
+              assert.deepEqual(gotCalls, [...Array(i + 1).keys()]);
+              resolve();
+            });
+          });
+        };
+        testHooks.push(hook);
+      }
+      assert.deepEqual(await hooks.aCallFirst(hookName), []);
+      assert.deepEqual(gotCalls, [0, 1, 2]);
+    });
+
+    it('default predicate: stops when satisfied', async function () {
+      testHooks.length = 0;
+      testHooks.push(makeHook(), makeHook('val1'), makeHook('val2'));
+      assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']);
+    });
+
+    it('default predicate: skips values that do not satisfy (undefined)', async function () {
+      testHooks.length = 0;
+      testHooks.push(makeHook(), makeHook('val1'));
+      assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']);
+    });
+
+    it('default predicate: skips values that do not satisfy (empty list)', async function () {
+      testHooks.length = 0;
+      testHooks.push(makeHook([]), makeHook('val1'));
+      assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']);
+    });
+
+    it('default predicate: null satisifes', async function () {
+      testHooks.length = 0;
+      testHooks.push(makeHook(null), makeHook('val1'));
+      assert.deepEqual(await hooks.aCallFirst(hookName), [null]);
+    });
+
+    it('custom predicate: called for each hook function', async function () {
+      testHooks.length = 0;
+      testHooks.push(makeHook(0), makeHook(1), makeHook(2));
+      let got = 0;
+      await hooks.aCallFirst(hookName, null, null, (val) => { ++got; return false; });
+      assert.equal(got, 3);
+    });
+
+    it('custom predicate: boolean false/true continues/stops iteration', async function () {
+      testHooks.length = 0;
+      testHooks.push(makeHook(1), makeHook(2), makeHook(3));
+      let nCall = 0;
+      const predicate = (val) => {
+        assert.deepEqual(val, [++nCall]);
+        return nCall === 2;
+      };
+      assert.deepEqual(await hooks.aCallFirst(hookName, null, null, predicate), [2]);
+      assert.equal(nCall, 2);
+    });
+
+    it('custom predicate: non-boolean falsy/truthy continues/stops iteration', async function () {
+      testHooks.length = 0;
+      testHooks.push(makeHook(1), makeHook(2), makeHook(3));
+      let nCall = 0;
+      const predicate = (val) => {
+        assert.deepEqual(val, [++nCall]);
+        return nCall === 2 ? {} : null;
+      };
+      assert.deepEqual(await hooks.aCallFirst(hookName, null, null, predicate), [2]);
+      assert.equal(nCall, 2);
+    });
+
+    it('custom predicate: array value passed unmodified to predicate', async function () {
+      const want = [0];
+      hook.hook_fn = () => want;
+      const predicate = (got) => { assert.equal(got, want); }; // Note: *NOT* deepEqual!
+      await hooks.aCallFirst(hookName, null, null, predicate);
+    });
+
+    it('custom predicate: normalized value passed to predicate (undefined)', async function () {
+      const predicate = (got) => { assert.deepEqual(got, []); };
+      await hooks.aCallFirst(hookName, null, null, predicate);
+    });
+
+    it('custom predicate: normalized value passed to predicate (null)', async function () {
+      hook.hook_fn = () => null;
+      const predicate = (got) => { assert.deepEqual(got, [null]); };
+      await hooks.aCallFirst(hookName, null, null, predicate);
+    });
+
+    it('non-empty arrays are returned unmodified', async function () {
+      const want = ['val1'];
+      testHooks.length = 0;
+      testHooks.push(makeHook(want), makeHook(['val2']));
+      assert.equal(await hooks.aCallFirst(hookName), want); // Note: *NOT* deepEqual!
+    });
+
+    it('value can be passed via callback', async function () {
+      const want = {};
+      hook.hook_fn = (hn, ctx, cb) => { cb(want); };
+      const got = await hooks.aCallFirst(hookName);
+      assert.deepEqual(got, [want]);
+      assert.equal(got[0], want); // Note: *NOT* deepEqual!
+    });
+  });
 });
diff --git a/tests/backend/specs/webaccess.js b/tests/backend/specs/webaccess.js
index 0843c3538..90558e327 100644
--- a/tests/backend/specs/webaccess.js
+++ b/tests/backend/specs/webaccess.js
@@ -1,11 +1,9 @@
 'use strict';
 
-function m(mod) { return `${__dirname}/../../../src/${mod}`; }
-
 const assert = require('assert').strict;
 const common = require('../common');
-const plugins = require(m('static/js/pluginfw/plugin_defs'));
-const settings = require(m('node/utils/Settings'));
+const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs');
+const settings = require('ep_etherpad-lite/node/utils/Settings');
 
 describe(__filename, function () {
   this.timeout(30000);
@@ -13,6 +11,13 @@ describe(__filename, function () {
   const backups = {};
   const authHookNames = ['preAuthorize', 'authenticate', 'authorize'];
   const failHookNames = ['preAuthzFailure', 'authnFailure', 'authzFailure', 'authFailure'];
+  const makeHook = (hookName, hookFn) => ({
+    hook_fn: hookFn,
+    hook_fn_name: `fake_plugin/${hookName}`,
+    hook_name: hookName,
+    part: {plugin: 'fake_plugin'},
+  });
+
   before(async function () { agent = await common.init(); });
   beforeEach(async function () {
     backups.hooks = {};
@@ -154,7 +159,10 @@ describe(__filename, function () {
         const h0 = new Handler(hookName, '_0');
         const h1 = new Handler(hookName, '_1');
         handlers[hookName] = [h0, h1];
-        plugins.hooks[hookName] = [{hook_fn: h0.handle.bind(h0)}, {hook_fn: h1.handle.bind(h1)}];
+        plugins.hooks[hookName] = [
+          makeHook(hookName, h0.handle.bind(h0)),
+          makeHook(hookName, h1.handle.bind(h1)),
+        ];
       }
     });
 
@@ -217,7 +225,7 @@ describe(__filename, function () {
         this.timeout(100);
         handlers.preAuthorize[0].innerHandle = () => [false];
         let called = false;
-        plugins.hooks.preAuthzFailure = [{hook_fn: (hookName, {req, res}, cb) => {
+        plugins.hooks.preAuthzFailure = [makeHook('preAuthzFailure', (hookName, {req, res}, cb) => {
           assert.equal(hookName, 'preAuthzFailure');
           assert(req != null);
           assert(res != null);
@@ -225,7 +233,7 @@ describe(__filename, function () {
           called = true;
           res.status(200).send('injected');
           return cb([true]);
-        }}];
+        })];
         await agent.get('/admin/').auth('admin', 'admin-password').expect(200, 'injected');
         assert(called);
       });
@@ -441,11 +449,11 @@ describe(__filename, function () {
     };
     const handlers = {};
 
-    beforeEach(function () {
+    beforeEach(async function () {
       failHookNames.forEach((hookName) => {
         const handler = new Handler(hookName);
         handlers[hookName] = handler;
-        plugins.hooks[hookName] = [{hook_fn: handler.handle.bind(handler)}];
+        plugins.hooks[hookName] = [makeHook(hookName, handler.handle.bind(handler))];
       });
       settings.requireAuthentication = true;
       settings.requireAuthorization = true;
diff --git a/tests/frontend/helper.js b/tests/frontend/helper.js
index 37c5af3b1..c38175fe1 100644
--- a/tests/frontend/helper.js
+++ b/tests/frontend/helper.js
@@ -1,5 +1,5 @@
 'use strict';
-const helper = {}; // eslint-disable-line
+const helper = {}; // eslint-disable-line no-redeclare
 
 (function () {
   let $iframe; const
@@ -181,7 +181,7 @@ const helper = {}; // eslint-disable-line
   };
 
   helper.waitFor = function (conditionFunc, timeoutTime = 1900, intervalTime = 10) {
-    const deferred = $.Deferred();  // eslint-disable-line
+    const deferred = new $.Deferred();
 
     const _fail = deferred.fail.bind(deferred);
     let listenForFail = false;
diff --git a/tests/frontend/helper/methods.js b/tests/frontend/helper/methods.js
index 4c7fe1204..157ba6aba 100644
--- a/tests/frontend/helper/methods.js
+++ b/tests/frontend/helper/methods.js
@@ -6,12 +6,12 @@
  */
 helper.spyOnSocketIO = function () {
   helper.contentWindow().pad.socket.on('message', (msg) => {
-    if (msg.type == 'COLLABROOM') {
-      if (msg.data.type == 'ACCEPT_COMMIT') {
+    if (msg.type === 'COLLABROOM') {
+      if (msg.data.type === 'ACCEPT_COMMIT') {
         helper.commits.push(msg);
-      } else if (msg.data.type == 'USER_NEWINFO') {
+      } else if (msg.data.type === 'USER_NEWINFO') {
         helper.userInfos.push(msg);
-      } else if (msg.data.type == 'CHAT_MESSAGE') {
+      } else if (msg.data.type === 'CHAT_MESSAGE') {
         helper.chatMessages.push(msg);
       }
     }
diff --git a/tests/frontend/helper/ui.js b/tests/frontend/helper/ui.js
index 0f3e64169..7ab8b990d 100644
--- a/tests/frontend/helper/ui.js
+++ b/tests/frontend/helper/ui.js
@@ -1,3 +1,5 @@
+'use strict';
+
 /**
  * the contentWindow is either the normal pad or timeslider
  *
diff --git a/tests/frontend/runner.js b/tests/frontend/runner.js
index 55ef2acf3..efc138883 100644
--- a/tests/frontend/runner.js
+++ b/tests/frontend/runner.js
@@ -170,7 +170,7 @@ $(() => {
     }
   });
 
-  // initalize the test helper
+  // initialize the test helper
   helper.init(() => {
     // configure and start the test framework
     const grep = getURLParameter('grep');
diff --git a/tests/frontend/specs/authorship_of_editions.js b/tests/frontend/specs/authorship_of_editions.js
index 539220417..c82ddd0fc 100644
--- a/tests/frontend/specs/authorship_of_editions.js
+++ b/tests/frontend/specs/authorship_of_editions.js
@@ -7,10 +7,11 @@ describe('author of pad edition', function () {
 
   // author 1 creates a new pad with some content (regular lines and lists)
   before(function (done) {
-    var padId = helper.newPad(() => {
+    const padId = helper.newPad(() => {
       // make sure pad has at least 3 lines
       const $firstLine = helper.padInner$('div').first();
-      const threeLines = ['regular line', 'line with ordered list', 'line with unordered list'].join('
'); + const threeLines = ['regular line', 'line with ordered list', 'line with unordered list'] + .join('
'); $firstLine.html(threeLines); // wait for lines to be processed by Etherpad @@ -45,7 +46,8 @@ describe('author of pad edition', function () { setTimeout(() => { // Expire cookie, so author is changed after reloading the pad. // See https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#Example_4_Reset_the_previous_cookie - helper.padChrome$.document.cookie = 'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; + helper.padChrome$.document.cookie = + 'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; helper.newPad(done, padId); }, 10000); @@ -62,26 +64,25 @@ describe('author of pad edition', function () { 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) { + + it('marks only the new content as changes of the second user on a ' + + 'line with ordered list', function (done) { this.timeout(1000); 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) { + it('marks only the new content as changes of the second user on ' + + 'a line with unordered list', function (done) { this.timeout(1000); changeLineAndCheckOnlyThatChangeIsFromThisAuthor(LINE_WITH_UNORDERED_LIST, 'z', done); }); /* ********************** Helper functions ************************ */ - var getLine = function (lineNumber) { - return helper.padInner$('div').eq(lineNumber); - }; + const getLine = (lineNumber) => helper.padInner$('div').eq(lineNumber); - const getAuthorFromClassList = function (classes) { - return classes.find((cls) => cls.startsWith('author')); - }; + const getAuthorFromClassList = (classes) => classes.find((cls) => cls.startsWith('author')); - var changeLineAndCheckOnlyThatChangeIsFromThisAuthor = function (lineNumber, textChange, done) { + const changeLineAndCheckOnlyThatChangeIsFromThisAuthor = (lineNumber, textChange, done) => { // get original author class const classes = getLine(lineNumber).find('span').first().attr('class').split(' '); const originalAuthor = getAuthorFromClassList(classes); diff --git a/tests/frontend/specs/bold.js b/tests/frontend/specs/bold.js index 8eee1186e..ac0d09a0f 100644 --- a/tests/frontend/specs/bold.js +++ b/tests/frontend/specs/bold.js @@ -22,8 +22,6 @@ describe('bold button', function () { const $boldButton = chrome$('.buttonicon-bold'); $boldButton.click(); - // ace creates a new dom element when you press a button - // so just get the first text element again const $newFirstTextElement = inner$('div').first(); // is there a element now? @@ -48,13 +46,11 @@ describe('bold button', function () { // select this text element $firstTextElement.sendkeys('{selectall}'); - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 66; // b inner$('#innerdocbody').trigger(e); - // ace creates a new dom element when you press a button - // so just get the first text element again const $newFirstTextElement = inner$('div').first(); // is there a element now? diff --git a/tests/frontend/specs/caret.js b/tests/frontend/specs/caret.js index 1fb8d8aa7..e5ce255b8 100644 --- a/tests/frontend/specs/caret.js +++ b/tests/frontend/specs/caret.js @@ -1,7 +1,9 @@ +'use strict'; + describe('As the caret is moved is the UI properly updated?', function () { + /* let padName; const numberOfRows = 50; - /* //create a new pad before each test run beforeEach(function(cb){ @@ -16,7 +18,8 @@ describe('As the caret is moved is the UI properly updated?', function () { */ /* Tests to do - * Keystroke up (38), down (40), left (37), right (39) with and without special keys IE control / shift + * Keystroke up (38), down (40), left (37), right (39) + * with and without special keys IE control / shift * Page up (33) / down (34) with and without special keys * Page up on the first line shouldn't move the viewport * Down down on the last line shouldn't move the viewport @@ -25,7 +28,9 @@ describe('As the caret is moved is the UI properly updated?', function () { */ /* Challenges - * How do we keep the authors focus on a line if the lines above the author are modified? We should only redraw the user to a location if they are typing and make sure shift and arrow keys aren't redrawing the UI else highlight - copy/paste would get broken + * How do we keep the authors focus on a line if the lines above the author are modified? + * We should only redraw the user to a location if they are typing and make sure shift + * and arrow keys aren't redrawing the UI else highlight - copy/paste would get broken * How can we simulate an edit event in the test framework? */ /* @@ -200,7 +205,8 @@ console.log(inner$); var chrome$ = helper.padChrome$; var numberOfRows = 50; - //ace creates a new dom element when you press a keystroke, so just get the first text element again + // ace creates a new dom element when you press a keystroke, + // so just get the first text element again var $newFirstTextElement = inner$("div").first(); var originalDivHeight = inner$("div").first().css("height"); prepareDocument(numberOfRows, $newFirstTextElement); // N lines into the first div as a target @@ -208,28 +214,33 @@ console.log(inner$); helper.waitFor(function(){ // Wait for the DOM to register the new items return inner$("div").first().text().length == 6; }).done(function(){ // Once the DOM has registered the items - inner$("div").each(function(index){ // Randomize the item heights (replicates images / headings etc) + // Randomize the item heights (replicates images / headings etc) + inner$("div").each(function(index){ var random = Math.floor(Math.random() * (50)) + 20; $(this).css("height", random+"px"); }); console.log(caretPosition(inner$)); var newDivHeight = inner$("div").first().css("height"); - var heightHasChanged = originalDivHeight != newDivHeight; // has the new div height changed from the original div height + // has the new div height changed from the original div height + var heightHasChanged = originalDivHeight != newDivHeight; expect(heightHasChanged).to.be(true); // expect the first line to be blank }); // Is this Element now visible to the pad user? helper.waitFor(function(){ // Wait for the DOM to register the new items - return isScrolledIntoView(inner$("div:nth-child("+numberOfRows+")"), inner$); // Wait for the DOM to scroll into place + // Wait for the DOM to scroll into place + return isScrolledIntoView(inner$("div:nth-child("+numberOfRows+")"), inner$); }).done(function(){ // Once the DOM has registered the items - inner$("div").each(function(index){ // Randomize the item heights (replicates images / headings etc) + // Randomize the item heights (replicates images / headings etc) + inner$("div").each(function(index){ var random = Math.floor(Math.random() * (80 - 20 + 1)) + 20; $(this).css("height", random+"px"); }); var newDivHeight = inner$("div").first().css("height"); - var heightHasChanged = originalDivHeight != newDivHeight; // has the new div height changed from the original div height + // has the new div height changed from the original div height + var heightHasChanged = originalDivHeight != newDivHeight; expect(heightHasChanged).to.be(true); // expect the first line to be blank }); var i = 0; @@ -241,7 +252,8 @@ console.log(inner$); // Does scrolling back up the pad with the up arrow show the correct contents? helper.waitFor(function(){ // Wait for the new position to be in place try{ - return isScrolledIntoView(inner$("div:nth-child("+numberOfRows+")"), inner$); // Wait for the DOM to scroll into place + // Wait for the DOM to scroll into place + return isScrolledIntoView(inner$("div:nth-child("+numberOfRows+")"), inner$); }catch(e){ return false; } @@ -256,7 +268,8 @@ console.log(inner$); // Does scrolling back up the pad with the up arrow show the correct contents? helper.waitFor(function(){ // Wait for the new position to be in place try{ - return isScrolledIntoView(inner$("div:nth-child(0)"), inner$); // Wait for the DOM to scroll into place + // Wait for the DOM to scroll into place + return isScrolledIntoView(inner$("div:nth-child(0)"), inner$); }catch(e){ return false; } @@ -276,7 +289,8 @@ console.log(inner$); // Does scrolling back up the pad with the up arrow show the correct contents? helper.waitFor(function(){ // Wait for the new position to be in place - return isScrolledIntoView(inner$("div:nth-child(1)"), inner$); // Wait for the DOM to scroll into place + // Wait for the DOM to scroll into place + return isScrolledIntoView(inner$("div:nth-child(1)"), inner$); }).done(function(){ // Once the DOM has registered the items expect(true).to.be(true); done(); @@ -284,17 +298,19 @@ console.log(inner$); */ }); -function prepareDocument(n, target) { // generates a random document with random content on n lines +// generates a random document with random content on n lines +const prepareDocument = (n, target) => { let i = 0; while (i < n) { // for each line target.sendkeys(makeStr()); // generate a random string and send that to the editor target.sendkeys('{enter}'); // generator an enter keypress i++; // rinse n times } -} +}; -function keyEvent(target, charCode, ctrl, shift) { // sends a charCode to the window - const e = target.Event(helper.evtType); +// sends a charCode to the window +const keyEvent = (target, charCode, ctrl, shift) => { + const e = new target.Event(helper.evtType); if (ctrl) { e.ctrlKey = true; // Control key } @@ -304,30 +320,33 @@ function keyEvent(target, charCode, ctrl, shift) { // sends a charCode to the wi e.which = charCode; e.keyCode = charCode; target('#innerdocbody').trigger(e); -} +}; -function makeStr() { // from http://stackoverflow.com/questions/1349404/generate-a-string-of-5-random-characters-in-javascript +// from http://stackoverflow.com/questions/1349404/generate-a-string-of-5-random-characters-in-javascript +const makeStr = () => { let text = ''; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < 5; i++) text += possible.charAt(Math.floor(Math.random() * possible.length)); return text; -} +}; -function isScrolledIntoView(elem, $) { // from http://stackoverflow.com/questions/487073/check-if-element-is-visible-after-scrolling +// from http://stackoverflow.com/questions/487073/check-if-element-is-visible-after-scrolling +const isScrolledIntoView = (elem, $) => { const docViewTop = $(window).scrollTop(); const docViewBottom = docViewTop + $(window).height(); const elemTop = $(elem).offset().top; // how far the element is from the top of it's container - let elemBottom = elemTop + $(elem).height(); // how far plus the height of the elem.. IE is it all in? + // how far plus the height of the elem.. IE is it all in? + let elemBottom = elemTop + $(elem).height(); elemBottom -= 16; // don't ask, sorry but this is needed.. return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop)); -} +}; -function caretPosition($) { +const caretPosition = ($) => { const doc = $.window.document; const pos = doc.getSelection(); pos.y = pos.anchorNode.parentElement.offsetTop; pos.x = pos.anchorNode.parentElement.offsetLeft; return pos; -} +}; diff --git a/tests/frontend/specs/change_user_color.js b/tests/frontend/specs/change_user_color.js index ca5935c58..1f41dcce2 100644 --- a/tests/frontend/specs/change_user_color.js +++ b/tests/frontend/specs/change_user_color.js @@ -7,7 +7,8 @@ describe('change user color', function () { this.timeout(60000); }); - it('Color picker remembers the user color after a refresh', function (done) { + it('Color picker matches original color and remembers the user color' + + ' after a refresh', function (done) { this.timeout(10000); const chrome$ = helper.padChrome$; @@ -95,7 +96,6 @@ describe('change user color', function () { // simulate a keypress of enter actually does evt.which = 10 not 13 $chatInput.sendkeys('{enter}'); - // check if chat shows up // wait until the chat message shows up helper.waitFor(() => chrome$('#chattext').children('p').length !== 0 ).done(() => { diff --git a/tests/frontend/specs/chat.js b/tests/frontend/specs/chat.js index 6f5e9b5a4..0026f0eb4 100644 --- a/tests/frontend/specs/chat.js +++ b/tests/frontend/specs/chat.js @@ -6,8 +6,10 @@ describe('Chat messages and UI', function () { helper.newPad(cb); }); - it('opens chat, sends a message, makes sure it exists and hides chat', async function () { + it('opens chat, sends a message, makes sure it exists ' + + 'on the page and hides chat', async function () { this.timeout(3000); + const chatValue = 'JohnMcLear'; await helper.showChat(); @@ -49,7 +51,8 @@ describe('Chat messages and UI', function () { expect(chat.text()).to.be(`${username}${time} ${chatValue}`); }); - it('makes chat stick to right side of the screen via settings, remove sticky via settings, close it', async function () { + it('makes chat stick to right side of the screen via settings, ' + + 'remove sticky via settings, close it', async function () { this.timeout(5000); await helper.showSettings(); @@ -66,8 +69,10 @@ describe('Chat messages and UI', function () { expect(helper.isChatboxShown()).to.be(false); }); - it('makes chat stick to right side of the screen via icon on the top right, remove sticky via icon, close it', async function () { + it('makes chat stick to right side of the screen via icon on the top' + + ' right, remove sticky via icon, close it', async function () { this.timeout(5000); + await helper.showChat(); await helper.enableStickyChatviaIcon(); @@ -83,7 +88,8 @@ describe('Chat messages and UI', function () { expect(helper.isChatboxShown()).to.be(false); }); - xit('Checks showChat=false URL Parameter shows/hides chat', function (done) { + xit('Checks showChat=false URL Parameter hides chat then' + + ' when removed it shows chat', function (done) { this.timeout(60000); setTimeout(() => { // give it a second to save the username on the server side diff --git a/tests/frontend/specs/clear_authorship_colors.js b/tests/frontend/specs/clear_authorship_colors.js index c0e116dc3..cac3daa9b 100644 --- a/tests/frontend/specs/clear_authorship_colors.js +++ b/tests/frontend/specs/clear_authorship_colors.js @@ -92,7 +92,7 @@ describe('clear authorship colors button', function () { let hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; expect(hasAuthorClass).to.be(false); - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 90; // z inner$('#innerdocbody').trigger(e); // shouldn't od anything diff --git a/tests/frontend/specs/delete.js b/tests/frontend/specs/delete.js index d2b9611c0..d3fc90de5 100644 --- a/tests/frontend/specs/delete.js +++ b/tests/frontend/specs/delete.js @@ -22,8 +22,6 @@ describe('delete keystroke', function () { $firstTextElement.sendkeys('{leftarrow}'); // simulate a keypress of the left arrow key $firstTextElement.sendkeys('{del}'); // simulate a keypress of delete - // ace creates a new dom element when you press a keystroke - // so just get the first text element again const $newFirstTextElement = inner$('div').first(); // get the new length of this element diff --git a/tests/frontend/specs/drag_and_drop.js b/tests/frontend/specs/drag_and_drop.js index 9fdbc5486..dff8e3193 100644 --- a/tests/frontend/specs/drag_and_drop.js +++ b/tests/frontend/specs/drag_and_drop.js @@ -86,16 +86,16 @@ describe('drag and drop', function () { const TARGET_LINE = 2; const FIRST_SOURCE_LINE = 5; - const getLine = function (lineNumber) { + const getLine = (lineNumber) => { const $lines = helper.padInner$('div'); return $lines.slice(lineNumber, lineNumber + 1); }; - const createScriptWithSeveralLines = function (done) { + const createScriptWithSeveralLines = (done) => { // create some lines to be used on the tests const $firstLine = helper.padInner$('div').first(); - $firstLine.html( - '...
...
Target line []
...
...
Source line 1.
Source line 2.
'); + $firstLine.html('...
...
Target line []
...
...
' + + 'Source line 1.
Source line 2.
'); // wait for lines to be split helper.waitFor(() => { @@ -104,7 +104,7 @@ describe('drag and drop', function () { }).done(done); }; - const selectPartOfSourceLine = function () { + const selectPartOfSourceLine = () => { const $sourceLine = getLine(FIRST_SOURCE_LINE); // select 'line 1' from 'Source line 1.' @@ -112,14 +112,14 @@ describe('drag and drop', function () { const end = start + 'line 1'.length; helper.selectLines($sourceLine, $sourceLine, start, end); }; - const selectMultipleSourceLines = function () { + const selectMultipleSourceLines = () => { const $firstSourceLine = getLine(FIRST_SOURCE_LINE); const $lastSourceLine = getLine(FIRST_SOURCE_LINE + 1); helper.selectLines($firstSourceLine, $lastSourceLine); }; - const dragSelectedTextAndDropItIntoMiddleOfLine = function (targetLineNumber) { + const dragSelectedTextAndDropItIntoMiddleOfLine = (targetLineNumber) => { // dragstart: start dragging content triggerEvent('dragstart'); @@ -132,7 +132,7 @@ describe('drag and drop', function () { triggerEvent('dragend'); }; - const getHtmlFromSelectedText = function () { + const getHtmlFromSelectedText = () => { const innerDocument = helper.padInner$.document; const range = innerDocument.getSelection().getRangeAt(0); @@ -145,12 +145,12 @@ describe('drag and drop', function () { return draggedHtml; }; - const triggerEvent = function (eventName) { - const event = helper.padInner$.Event(eventName); + const triggerEvent = (eventName) => { + const event = new helper.padInner$.Event(eventName); helper.padInner$('#innerdocbody').trigger(event); }; - const moveSelectionIntoTarget = function (draggedHtml, targetLineNumber) { + const moveSelectionIntoTarget = (draggedHtml, targetLineNumber) => { const innerDocument = helper.padInner$.document; // delete original content diff --git a/tests/frontend/specs/helper.js b/tests/frontend/specs/helper.js index 60f8dd331..cb72fcf7a 100644 --- a/tests/frontend/specs/helper.js +++ b/tests/frontend/specs/helper.js @@ -7,7 +7,7 @@ describe('the test helper', function () { let times = 10; - const loadPad = function () { + const loadPad = () => { helper.newPad(() => { times--; if (times > 0) { @@ -77,13 +77,14 @@ describe('the test helper', function () { // Before refreshing, make sure the name is there expect($usernameInput.val()).to.be('John McLear'); - // Now that we have a chrome, we can set a pad cookie, so we can confirm it gets wiped as well + // Now that we have a chrome, we can set a pad cookie + // so we can confirm it gets wiped as well chrome$.document.cookie = 'prefsHtml=baz;expires=Thu, 01 Jan 3030 00:00:00 GMT'; expect(chrome$.document.cookie).to.contain('prefsHtml=baz'); - // Cookies are weird. Because it's attached to chrome$ (as helper.setPadCookies does), AND we - // didn't put path=/, we shouldn't expect it to be visible on window.document.cookie. Let's just - // be sure. + // Cookies are weird. Because it's attached to chrome$ (as helper.setPadCookies does) + // AND we didn't put path=/, we shouldn't expect it to be visible on + // window.document.cookie. Let's just be sure. expect(window.document.cookie).to.not.contain('prefsHtml=baz'); setTimeout(() => { // give it a second to save the username on the server side @@ -268,7 +269,8 @@ describe('the test helper', function () { this.timeout(60000); }); - it('changes editor selection to be between startOffset of $startLine and endOffset of $endLine', function (done) { + it('changes editor selection to be between startOffset of $startLine ' + + 'and endOffset of $endLine', function (done) { const inner$ = helper.padInner$; const startOffset = 2; @@ -315,7 +317,8 @@ describe('the test helper', function () { * is not consistent between browsers but that's the situation so that's * how I'm covering it in this test. */ - expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines to test'); + expect(cleanText( + selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines to test'); done(); }); @@ -367,12 +370,14 @@ describe('the test helper', function () { * is not consistent between browsers but that's the situation so that's * how I'm covering it in this test. */ - expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines to test'); + expect(cleanText( + selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines to test'); done(); }); - it('selects all text between beginning of $startLine and end of $endLine when no offset is provided', function (done) { + it('selects all text between beginning of $startLine and end of $endLine ' + + 'when no offset is provided', function (done) { const inner$ = helper.padInner$; const $lines = inner$('div'); @@ -390,7 +395,8 @@ describe('the test helper', function () { * is not consistent between browsers but that's the situation so that's * how I'm covering it in this test. */ - expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('short lines to test'); + expect(cleanText( + selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('short lines to test'); done(); }); diff --git a/tests/frontend/specs/importexport.js b/tests/frontend/specs/importexport.js index 0be2a0744..4eb95eeb0 100644 --- a/tests/frontend/specs/importexport.js +++ b/tests/frontend/specs/importexport.js @@ -1,3 +1,5 @@ +'use strict'; + describe('import functionality', function () { beforeEach(function (cb) { helper.newPad(cb); // creates a new pad @@ -16,7 +18,6 @@ describe('import functionality', function () { return newtext; } function importrequest(data, importurl, type) { - let success; let error; const result = $.ajax({ url: importurl, @@ -27,7 +28,17 @@ describe('import functionality', function () { accepts: { text: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', }, - data: `Content-Type: multipart/form-data; boundary=--boundary\r\n\r\n--boundary\r\nContent-Disposition: form-data; name="file"; filename="import.${type}"\r\nContent-Type: text/plain\r\n\r\n${data}\r\n\r\n--boundary`, + data: [ + 'Content-Type: multipart/form-data; boundary=--boundary', + '', + '--boundary', + `Content-Disposition: form-data; name="file"; filename="import.${type}"`, + 'Content-Type: text/plain', + '', + data, + '', + '--boundary', + ].join('\r\n'), error(res) { error = res; }, @@ -56,7 +67,8 @@ describe('import functionality', function () { const importurl = `${helper.padChrome$.window.location.href}/import`; const textWithNewLines = 'imported text\nnewline'; importrequest(textWithNewLines, importurl, 'txt'); - helper.waitFor(() => expect(getinnertext()).to.be('imported text\nnewline\n
\n')); + helper.waitFor(() => expect(getinnertext()) + .to.be('imported text\nnewline\n
\n')); const results = exportfunc(helper.padChrome$.window.location.href); expect(results[0][1]).to.be('imported text
newline

'); expect(results[1][1]).to.be('imported text\nnewline\n\n'); @@ -66,7 +78,8 @@ describe('import functionality', function () { const importurl = `${helper.padChrome$.window.location.href}/import`; const htmlWithNewLines = 'htmltext
newline'; importrequest(htmlWithNewLines, importurl, 'html'); - helper.waitFor(() => expect(getinnertext()).to.be('htmltext\nnewline\n
\n')); + helper.waitFor(() => expect(getinnertext()) + .to.be('htmltext\nnewline\n
\n')); const results = exportfunc(helper.padChrome$.window.location.href); expect(results[0][1]).to.be('htmltext
newline

'); expect(results[1][1]).to.be('htmltext\nnewline\n\n'); @@ -74,69 +87,109 @@ describe('import functionality', function () { }); xit('import a pad with attributes from html', function (done) { const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithNewLines = 'htmltext
newline'; + const htmlWithNewLines = 'htmltext
' + + 'newline'; importrequest(htmlWithNewLines, importurl, 'html'); - helper.waitFor(() => expect(getinnertext()).to.be('htmltext\nnewline\n
\n')); + helper.waitFor(() => expect(getinnertext()) + .to.be('htmltext\n' + + 'newline\n
\n')); const results = exportfunc(helper.padChrome$.window.location.href); - expect(results[0][1]).to.be('htmltext
newline

'); + expect(results[0][1]) + .to.be('htmltext
newline

'); expect(results[1][1]).to.be('htmltext\nnewline\n\n'); done(); }); xit('import a pad with bullets from html', function (done) { const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithBullets = '
  • bullet line 1
  • bullet line 2
    • bullet2 line 1
    • bullet2 line 2
'; + const htmlWithBullets = '
  • bullet line 1
  • ' + + '
  • bullet line 2
    • bullet2 line 1
    • ' + + '
    • bullet2 line 2
'; importrequest(htmlWithBullets, importurl, 'html'); - helper.waitFor(() => expect(getinnertext()).to.be('\ -
  • bullet line 1
\n\ -
  • bullet line 2
\n\ -
  • bullet2 line 1
\n\ -
  • bullet2 line 2
\n\ -
\n')); + helper.waitFor(() => expect(getinnertext()).to.be( + '
  • bullet line 1
\n' + + '
  • bullet line 2
\n' + + '
  • bullet2 line 1
\n' + + '
  • bullet2 line 2
\n' + + '
\n')); const results = exportfunc(helper.padChrome$.window.location.href); - expect(results[0][1]).to.be('
  • bullet line 1
  • bullet line 2
    • bullet2 line 1
    • bullet2 line 2

'); - expect(results[1][1]).to.be('\t* bullet line 1\n\t* bullet line 2\n\t\t* bullet2 line 1\n\t\t* bullet2 line 2\n\n'); + expect(results[0][1]).to.be( + '
  • bullet line 1
  • bullet line 2
  • ' + + '
    • bullet2 line 1
    • bullet2 line 2

'); + expect(results[1][1]) + .to.be('\t* bullet line 1\n\t* bullet line 2\n' + + '\t\t* bullet2 line 1\n\t\t* bullet2 line 2\n\n'); done(); }); xit('import a pad with bullets and newlines from html', function (done) { const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithBullets = '
  • bullet line 1

  • bullet line 2
    • bullet2 line 1

    • bullet2 line 2
'; + const htmlWithBullets = '
  • bullet line 1
  • ' + + '

  • bullet line 2
    • ' + + '
    • bullet2 line 1

    ' + + '
    • bullet2 line 2
'; importrequest(htmlWithBullets, importurl, 'html'); - helper.waitFor(() => expect(getinnertext()).to.be('\ -
  • bullet line 1
\n\ -
\n\ -
  • bullet line 2
\n\ -
  • bullet2 line 1
\n\ -
\n\ -
  • bullet2 line 2
\n\ -
\n')); + helper.waitFor(() => expect(getinnertext()).to.be( + '
  • bullet line 1
\n' + + '
\n' + + '
  • bullet line 2
\n' + + '
  • bullet2 line 1
\n' + + '
\n' + + '
  • bullet2 line 2
\n' + + '
\n')); const results = exportfunc(helper.padChrome$.window.location.href); - expect(results[0][1]).to.be('
  • bullet line 1

  • bullet line 2
    • bullet2 line 1

    • bullet2 line 2

'); - expect(results[1][1]).to.be('\t* bullet line 1\n\n\t* bullet line 2\n\t\t* bullet2 line 1\n\n\t\t* bullet2 line 2\n\n'); + expect(results[0][1]).to.be( + '
  • bullet line 1

    ' + + '
  • bullet line 2
    • bullet2 line 1
    ' + + '

    • bullet2 line 2

'); + expect(results[1][1]).to.be( + '\t* bullet line 1\n\n\t* bullet line 2\n\t\t* bullet2 line 1\n\n\t\t* bullet2 line 2\n\n'); done(); }); xit('import a pad with bullets and newlines and attributes from html', function (done) { const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithBullets = '
  • bullet line 1

  • bullet line 2
    • bullet2 line 1

        • bullet4 line 2 bisu
        • bullet4 line 2 bs
        • bullet4 line 2 uuis
'; + const htmlWithBullets = '
  • bullet line 1
  • ' + + '

  • bullet line 2
  • ' + + '
    • bullet2 line 1
' + + '
        ' + + '
        • ' + + 'bullet4 line 2 bisu
        • ' + + 'bullet4 line 2 bs
        • ' + + '
        • bullet4 line 2 u' + + 'uis
'; importrequest(htmlWithBullets, importurl, 'html'); - helper.waitFor(() => expect(getinnertext()).to.be('\ -
  • bullet line 1
\n\
\n\ -
  • bullet line 2
\n\ -
  • bullet2 line 1
\n
\n\ -
  • bullet4 line 2 bisu
\n\ -
  • bullet4 line 2 bs
\n\ -
  • bullet4 line 2 uuis
\n\ -
\n')); + helper.waitFor(() => expect(getinnertext()).to.be( + '
  • bullet line 1
\n
\n' + + '
  • bullet line 2
\n' + + '
  • bullet2 line 1
\n
\n' + + '
  • ' + + 'bullet4 line 2 bisu
\n' + + '
  • ' + + 'bullet4 line 2 bs
\n' + + '
  • bullet4 line 2 u' + + 'uis
\n' + + '
\n')); const results = exportfunc(helper.padChrome$.window.location.href); - expect(results[0][1]).to.be('
  • bullet line 1

  • bullet line 2
    • bullet2 line 1

        • bullet4 line 2 bisu
        • bullet4 line 2 bs
        • bullet4 line 2 uuis

'); - expect(results[1][1]).to.be('\t* bullet line 1\n\n\t* bullet line 2\n\t\t* bullet2 line 1\n\n\t\t\t\t* bullet4 line 2 bisu\n\t\t\t\t* bullet4 line 2 bs\n\t\t\t\t* bullet4 line 2 uuis\n\n'); + expect(results[0][1]).to.be( + '
  • bullet line 1
' + + '
  • bullet line 2
    • bullet2 line 1
    • ' + + '

        • bullet4 line 2 bisu' + + '
        • bullet4 line 2 bs' + + '
        • bullet4 line 2 uuis

'); + expect(results[1][1]).to.be( + '\t* bullet line 1\n\n\t* bullet line 2\n\t\t* bullet2 line 1\n\n\t\t\t\t* bullet4 line 2' + + ' bisu\n\t\t\t\t* bullet4 line 2 bs\n\t\t\t\t* bullet4 line 2 uuis\n\n'); done(); }); xit('import a pad with nested bullets from html', function (done) { const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithBullets = '
  • bullet line 1
  • bullet line 2
    • bullet2 line 1
        • bullet4 line 2
        • bullet4 line 2
        • bullet4 line 2
      • bullet3 line 1
  • bullet2 line 1
'; + const htmlWithBullets = '
  • bullet line 1
  • ' + + '
  • bullet line 2
    • ' + + '
    • bullet2 line 1
      ' + + '
        • bullet4 line 2
        • ' + + '
        • bullet4 line 2
        • bullet4 line 2
      • bullet3 line 1
      ' + + '
  • bullet2 line 1
'; importrequest(htmlWithBullets, importurl, 'html'); const oldtext = getinnertext(); - helper.waitFor(() => oldtext != getinnertext() + helper.waitFor(() => oldtext !== getinnertext() // return expect(getinnertext()).to.be('\ //
  • bullet line 1
\n\ //
  • bullet line 2
\n\ @@ -148,73 +201,127 @@ describe('import functionality', function () { ); const results = exportfunc(helper.padChrome$.window.location.href); - expect(results[0][1]).to.be('
  • bullet line 1
  • bullet line 2
    • bullet2 line 1
        • bullet4 line 2
        • bullet4 line 2
        • bullet4 line 2
      • bullet3 line 1
  • bullet2 line 1

'); - expect(results[1][1]).to.be('\t* bullet line 1\n\t* bullet line 2\n\t\t* bullet2 line 1\n\t\t\t\t* bullet4 line 2\n\t\t\t\t* bullet4 line 2\n\t\t\t\t* bullet4 line 2\n\t\t\t* bullet3 line 1\n\t* bullet2 line 1\n\n'); + expect(results[0][1]).to.be( + '
  • bullet line 1
  • bullet line 2
  • ' + + '
    • bullet2 line 1
        • bullet4 line 2
        • ' + + '
        • bullet4 line 2
        • bullet4 line 2
      • bullet3 line 1
    ' + + '
  • bullet2 line 1

'); + expect(results[1][1]).to.be( + '\t* bullet line 1\n\t* bullet line 2\n\t\t* bullet2 line 1\n\t\t\t\t* bullet4 line 2' + + '\n\t\t\t\t* bullet4 line 2\n\t\t\t\t* bullet4 line 2\n\t\t\t* bullet3 line 1' + + '\n\t* bullet2 line 1\n\n'); done(); }); - xit('import a pad with 8 levels of bullets and newlines and attributes from html', function (done) { + xit('import with 8 levels of bullets and newlines and attributes from html', function (done) { const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithBullets = '
  • bullet line 1

  • bullet line 2
    • bullet2 line 1

        • bullet4 line 2 bisu
        • bullet4 line 2 bs
        • bullet4 line 2 uuis
                • foo
                • foobar bs
          • foobar
    '; + const htmlWithBullets = + '
    • bullet line 1
    • ' + + '

    • bullet line 2
      • ' + + 'bullet2 line 1

        ' + + '
          • ' + + 'bullet4 line 2 bisu
          • ' + + 'bullet4 line 2 bs
          • bullet4 line 2 u' + + 'uis
          • ' + + '
                  ' + + '
                  • foo
                  • ' + + 'foobar bs
              ' + + '
            • foobar
      '; importrequest(htmlWithBullets, importurl, 'html'); - helper.waitFor(() => expect(getinnertext()).to.be('\ -
      • bullet line 1
      \n\
      \n\ -
      • bullet line 2
      \n\ -
      • bullet2 line 1
      \n
      \n\ -
      • bullet4 line 2 bisu
      \n\ -
      • bullet4 line 2 bs
      \n\ -
      • bullet4 line 2 uuis
      \n\ -
      • foo
      \n\ -
      • foobar bs
      \n\ -
      • foobar
      \n\ -
      \n')); + helper.waitFor(() => expect(getinnertext()).to.be( + '
      • bullet line 1
      \n
      \n' + + '
      • bullet line 2
      \n' + + '
      • bullet2 line 1
      \n
      \n' + + '
      • bullet4 line 2 bisu' + + '
      \n' + + '
      • bullet4 line 2 bs' + + '
      \n' + + '
      • bullet4 line 2 u' + + 'uis' + + '
      \n' + + '
      • foo
      \n' + + '
      • foobar bs' + + '
      \n' + + '
      • foobar
      \n' + + '
      \n')); const results = exportfunc(helper.padChrome$.window.location.href); - expect(results[0][1]).to.be('
      • bullet line 1

      • bullet line 2
        • bullet2 line 1

            • bullet4 line 2 bisu
            • bullet4 line 2 bs
            • bullet4 line 2 uuis
                    • foo
                    • foobar bs
              • foobar

      '); - expect(results[1][1]).to.be('\t* bullet line 1\n\n\t* bullet line 2\n\t\t* bullet2 line 1\n\n\t\t\t\t* bullet4 line 2 bisu\n\t\t\t\t* bullet4 line 2 bs\n\t\t\t\t* bullet4 line 2 uuis\n\t\t\t\t\t\t\t\t* foo\n\t\t\t\t\t\t\t\t* foobar bs\n\t\t\t\t\t* foobar\n\n'); + expect(results[0][1]).to.be( + '
      • bullet line 1

        ' + + '
      • bullet line 2
        • bullet2 line 1
      ' + + '
            • ' + + 'bullet4 line 2 bisu
            • ' + + 'bullet4 line 2 bs
            • bullet4 line 2 u' + + 'uis
                    • foo
                    • ' + + '
                    • foobar bs
              • foobar
              • ' + + '

      '); + expect(results[1][1]).to.be( + '\t* bullet line 1\n\n\t* bullet line 2\n\t\t* ' + + 'bullet2 line 1\n\n\t\t\t\t* bullet4 line 2 bisu\n\t\t\t\t* bullet4 line 2 ' + + 'bs\n\t\t\t\t* bullet4 line 2 uuis\n\t\t\t\t\t\t\t\t* foo\n\t\t\t\t\t\t\t\t* ' + + 'foobar bs\n\t\t\t\t\t* foobar\n\n'); done(); }); xit('import a pad with ordered lists from html', function (done) { const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithBullets = '
      1. number 1 line 1
      1. number 2 line 2
      '; + const htmlWithBullets = '
        ' + + '
      1. number 1 line 1
        ' + + '
      1. number 2 line 2
      '; importrequest(htmlWithBullets, importurl, 'html'); console.error(getinnertext()); - expect(getinnertext()).to.be('\ -
      1. number 1 line 1
      \n\ -
      1. number 2 line 2
      \n\ -
      \n'); + expect(getinnertext()).to.be( + '
      1. number 1 line 1
      \n' + + '
      1. number 2 line 2
      \n' + + '
      \n'); const results = exportfunc(helper.padChrome$.window.location.href); - expect(results[0][1]).to.be('
      1. number 1 line 1
      1. number 2 line 2
      '); + expect(results[0][1]).to.be( + '
      1. number 1 line 1
      2. ' + + '
      1. number 2 line 2
      '); expect(results[1][1]).to.be(''); done(); }); xit('import a pad with ordered lists and newlines from html', function (done) { const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithBullets = '
      1. number 9 line 1

      1. number 10 line 2
        1. number 2 times line 1

        1. number 2 times line 2
      '; + const htmlWithBullets = '
        ' + + '
      1. number 9 line 1

        ' + + '
      1. number 10 line 2
        1. ' + + '
        2. number 2 times line 1

        ' + + '
        1. number 2 times line 2
      '; importrequest(htmlWithBullets, importurl, 'html'); - expect(getinnertext()).to.be('\ -
      1. number 9 line 1
      \n\ -
      \n\ -
      1. number 10 line 2
      \n\ -
      1. number 2 times line 1
      \n\ -
      \n\ -
      1. number 2 times line 2
      \n\ -
      \n'); + expect(getinnertext()).to.be( + '
      1. number 9 line 1
      \n' + + '
      \n' + + '
      1. number 10 line 2
      2. ' + + '
      \n' + + '
      1. number 2 times line 1
      \n' + + '
      \n' + + '
      1. number 2 times line 2
      \n' + + '
      \n'); const results = exportfunc(helper.padChrome$.window.location.href); console.error(results); done(); }); - xit('import a pad with nested ordered lists and attributes and newlines from html', function (done) { + xit('import with nested ordered lists and attributes and newlines from html', function (done) { const importurl = `${helper.padChrome$.window.location.href}/import`; - const htmlWithBullets = '
      1. bold strikethrough italics underline line 1bold

      1. number 10 line 2
        1. number 2 times line 1

        1. number 2 times line 2
      '; + const htmlWithBullets = '
      1. ' + + 'bold strikethrough italics underline' + + ' line 1bold
      2. ' + + '

        ' + + '
      1. number 10 line 2
        1. ' + + '
        2. number 2 times line 1

      ' + + '
          ' + + '
        1. number 2 times line 2
      '; importrequest(htmlWithBullets, importurl, 'html'); - expect(getinnertext()).to.be('\ -
      1. bold strikethrough italics underline line 1bold
      \n\ -
      \n\ -
      1. number 10 line 2
      \n\ -
      1. number 2 times line 1
      \n\ -
      \n\ -
      1. number 2 times line 2
      \n\ -
      \n'); + expect(getinnertext()).to.be( + '
      1. ' + + 'bold strikethrough italics underline' + + ' line 1bold
      \n' + + '
      \n' + + '
      1. number 10 line 2
      \n' + + '
      1. ' + + 'number 2 times line 1
      \n' + + '
      \n' + + '
      1. number 2 times line 2
      \n' + + '
      \n'); const results = exportfunc(helper.padChrome$.window.location.href); console.error(results); done(); diff --git a/tests/frontend/specs/importindents.js b/tests/frontend/specs/importindents.js index 6209236df..eecbbce59 100644 --- a/tests/frontend/specs/importindents.js +++ b/tests/frontend/specs/importindents.js @@ -1,3 +1,5 @@ +'use strict'; + describe('import indents functionality', function () { beforeEach(function (cb) { helper.newPad(cb); // creates a new pad @@ -13,7 +15,6 @@ describe('import indents functionality', function () { return newtext; } function importrequest(data, importurl, type) { - let success; let error; const result = $.ajax({ url: importurl, @@ -24,7 +25,17 @@ describe('import indents functionality', function () { accepts: { text: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', }, - data: `Content-Type: multipart/form-data; boundary=--boundary\r\n\r\n--boundary\r\nContent-Disposition: form-data; name="file"; filename="import.${type}"\r\nContent-Type: text/plain\r\n\r\n${data}\r\n\r\n--boundary`, + data: [ + 'Content-Type: multipart/form-data; boundary=--boundary', + '', + '--boundary', + `Content-Disposition: form-data; name="file"; filename="import.${type}"`, + 'Content-Type: text/plain', + '', + data, + '', + '--boundary', + ].join('\r\n'), error(res) { error = res; }, @@ -51,54 +62,67 @@ describe('import indents functionality', function () { xit('import a pad with indents from html', function (done) { const importurl = `${helper.padChrome$.window.location.href}/import`; + /* eslint-disable-next-line max-len */ const htmlWithIndents = '
      • indent line 1
      • indent line 2
        • indent2 line 1
        • indent2 line 2
      '; importrequest(htmlWithIndents, importurl, 'html'); - helper.waitFor(() => expect(getinnertext()).to.be('\ -
      • indent line 1
      \n\ -
      • indent line 2
      \n\ -
      • indent2 line 1
      \n\ -
      • indent2 line 2
      \n\ -
      \n')); + helper.waitFor(() => expect(getinnertext()).to.be( + '
      • indent line 1
      \n' + + '
      • indent line 2
      \n' + + '
      • indent2 line 1
      \n' + + '
      • indent2 line 2
      \n' + + '
      \n')); const results = exportfunc(helper.padChrome$.window.location.href); + /* eslint-disable-next-line max-len */ expect(results[0][1]).to.be('
      • indent line 1
      • indent line 2
        • indent2 line 1
        • indent2 line 2

      '); - expect(results[1][1]).to.be('\tindent line 1\n\tindent line 2\n\t\tindent2 line 1\n\t\tindent2 line 2\n\n'); + expect(results[1][1]) + .to.be('\tindent line 1\n\tindent line 2\n\t\tindent2 line 1\n\t\tindent2 line 2\n\n'); done(); }); xit('import a pad with indented lists and newlines from html', function (done) { const importurl = `${helper.padChrome$.window.location.href}/import`; + /* eslint-disable-next-line max-len */ const htmlWithIndents = '
      • indent line 1

      • indent 1 line 2
        • indent 2 times line 1

        • indent 2 times line 2
      '; importrequest(htmlWithIndents, importurl, 'html'); - helper.waitFor(() => expect(getinnertext()).to.be('\ -
      • indent line 1
      \n\ -
      \n\ -
      • indent 1 line 2
      \n\ -
      • indent 2 times line 1
      \n\ -
      \n\ -
      • indent 2 times line 2
      \n\ -
      \n')); + helper.waitFor(() => expect(getinnertext()).to.be( + '
      • indent line 1
      \n' + + '
      \n' + + '
      • indent 1 line 2
      \n' + + '
      • indent 2 times line 1
      \n' + + '
      \n' + + '
      • indent 2 times line 2
      \n' + + '
      \n')); const results = exportfunc(helper.padChrome$.window.location.href); + /* eslint-disable-next-line max-len */ expect(results[0][1]).to.be('
      • indent line 1

      • indent 1 line 2
        • indent 2 times line 1

        • indent 2 times line 2

      '); + /* eslint-disable-next-line max-len */ expect(results[1][1]).to.be('\tindent line 1\n\n\tindent 1 line 2\n\t\tindent 2 times line 1\n\n\t\tindent 2 times line 2\n\n'); done(); }); - xit('import a pad with 8 levels of indents and newlines and attributes from html', function (done) { + xit('import with 8 levels of indents and newlines and attributes from html', function (done) { const importurl = `${helper.padChrome$.window.location.href}/import`; + /* eslint-disable-next-line max-len */ const htmlWithIndents = '
      • indent line 1

      • indent line 2
        • indent2 line 1

            • indent4 line 2 bisu
            • indent4 line 2 bs
            • indent4 line 2 uuis
                    • foo
                    • foobar bs
              • foobar
        '; importrequest(htmlWithIndents, importurl, 'html'); - helper.waitFor(() => expect(getinnertext()).to.be('\ -
        • indent line 1
        \n\
        \n\ -
        • indent line 2
        \n\ -
        • indent2 line 1
        \n
        \n\ -
        • indent4 line 2 bisu
        \n\ -
        • indent4 line 2 bs
        \n\ -
        • indent4 line 2 uuis
        \n\ -
        • foo
        \n\ -
        • foobar bs
        \n\ -
        • foobar
        \n\ -
        \n')); + helper.waitFor(() => expect(getinnertext()).to.be( + '
        • indent line 1
        \n
        \n' + + '
        • indent line 2
        \n' + + '
        • indent2 line 1
        \n
        \n' + + '
        • indent4 ' + + 'line 2 bisu
        \n' + + '
        • ' + + 'indent4 line 2 bs
        \n' + + '
        • indent4 line 2 u' + + 'uis
        \n' + + '
        • foo
        \n' + + '
        • foobar bs' + + '
        \n' + + '
        • foobar
        \n' + + '
        \n')); const results = exportfunc(helper.padChrome$.window.location.href); + /* eslint-disable-next-line max-len */ expect(results[0][1]).to.be('
        • indent line 1

        • indent line 2
          • indent2 line 1

              • indent4 line 2 bisu
              • indent4 line 2 bs
              • indent4 line 2 uuis
                      • foo
                      • foobar bs
                • foobar

        '); + /* eslint-disable-next-line max-len */ expect(results[1][1]).to.be('\tindent line 1\n\n\tindent line 2\n\t\tindent2 line 1\n\n\t\t\t\tindent4 line 2 bisu\n\t\t\t\tindent4 line 2 bs\n\t\t\t\tindent4 line 2 uuis\n\t\t\t\t\t\t\t\tfoo\n\t\t\t\t\t\t\t\tfoobar bs\n\t\t\t\t\tfoobar\n\n'); done(); }); diff --git a/tests/frontend/specs/indentation.js b/tests/frontend/specs/indentation.js index b7c173660..377b1af74 100644 --- a/tests/frontend/specs/indentation.js +++ b/tests/frontend/specs/indentation.js @@ -17,7 +17,7 @@ describe('indentation button', function () { // select this text element $firstTextElement.sendkeys('{selectall}'); - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.keyCode = 9; // tab :| inner$('#innerdocbody').trigger(e); @@ -60,7 +60,8 @@ describe('indentation button', function () { }); }); - it("indents text with spaces on enter if previous line ends with ':', '[', '(', or '{'", function (done) { + it('indents text with spaces on enter if previous line ends ' + + "with ':', '[', '(', or '{'", function (done) { this.timeout(1200); const inner$ = helper.padInner$; @@ -111,7 +112,8 @@ describe('indentation button', function () { }); }); - it("appends indentation to the indent of previous line if previous line ends with ':', '[', '(', or '{'", function (done) { + it('appends indentation to the indent of previous line if previous line ends ' + + "with ':', '[', '(', or '{'", function (done) { this.timeout(1200); const inner$ = helper.padInner$; @@ -136,7 +138,8 @@ describe('indentation button', function () { }); }); - it("issue #2772 shows '*' when multiple indented lines receive a style and are outdented", async function () { + it("issue #2772 shows '*' when multiple indented lines " + + ' receive a style and are outdented', async function () { this.timeout(1200); const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -189,7 +192,6 @@ describe('indentation button', function () { var $indentButton = testHelper.$getPadChrome().find(".buttonicon-indent"); $indentButton.click(); - //ace creates a new dom element when you press a button, so just get the first text element again var newFirstTextElement = $inner.find("div").first(); // is there a list-indent class element now? @@ -227,7 +229,6 @@ describe('indentation button', function () { $outdentButton.click(); $outdentButton.click(); - //ace creates a new dom element when you press a button, so just get the first text element again var newFirstTextElement = $inner.find("div").first(); // is there a list-indent class element now? @@ -274,7 +275,9 @@ describe('indentation button', function () { //get the second text element out of the inner iframe setTimeout(function(){ // THIS IS REALLY BAD - var secondTextElement = $('iframe').contents().find('iframe').contents().find('iframe').contents().find('body > div').get(1); // THIS IS UGLY + var secondTextElement = $('iframe').contents() + .find('iframe').contents() + .find('iframe').contents().find('body > div').get(1); // THIS IS UGLY // is there a list-indent class element now? var firstChild = secondTextElement.children(":first"); @@ -289,7 +292,10 @@ describe('indentation button', function () { expect(isLI).to.be(true); //get the first text element out of the inner iframe - var thirdTextElement = $('iframe').contents().find('iframe').contents().find('iframe').contents().find('body > div').get(2); // THIS IS UGLY TOO + var thirdTextElement = $('iframe').contents() + .find('iframe').contents() + .find('iframe').contents() + .find('body > div').get(2); // THIS IS UGLY TOO // is there a list-indent class element now? var firstChild = thirdTextElement.children(":first"); @@ -309,7 +315,7 @@ describe('indentation button', function () { const pressEnter = () => { const inner$ = helper.padInner$; - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.keyCode = 13; // enter :| inner$('#innerdocbody').trigger(e); }; diff --git a/tests/frontend/specs/italic.js b/tests/frontend/specs/italic.js index b9edbda98..5ab6d652c 100644 --- a/tests/frontend/specs/italic.js +++ b/tests/frontend/specs/italic.js @@ -21,7 +21,7 @@ describe('italic some text', function () { // get the bold button and click it const $boldButton = chrome$('.buttonicon-italic'); $boldButton.click(); - + const $newFirstTextElement = inner$('div').first(); // is there a element now? @@ -46,7 +46,7 @@ describe('italic some text', function () { // select this text element $firstTextElement.sendkeys('{selectall}'); - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 105; // i inner$('#innerdocbody').trigger(e); diff --git a/tests/frontend/specs/multiple_authors_clear_authorship_colors.js b/tests/frontend/specs/multiple_authors_clear_authorship_colors.js index e658ff19f..a0e6b1aef 100755 --- a/tests/frontend/specs/multiple_authors_clear_authorship_colors.js +++ b/tests/frontend/specs/multiple_authors_clear_authorship_colors.js @@ -15,8 +15,8 @@ describe('author of pad edition', function () { setTimeout(() => { // Expire cookie, so author is changed after reloading the pad. // See https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#Example_4_Reset_the_previous_cookie - const cookieVal = 'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; - helper.padChrome$.document.cookie = cookieVal; + helper.padChrome$.document.cookie = + 'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; helper.newPad(done, padId); }, 1000); @@ -25,7 +25,12 @@ describe('author of pad edition', function () { this.timeout(60000); }); - const clearAuthorship = function (done) { + // author 2 makes some changes on the pad + it('Clears Authorship by second user', function (done) { + clearAuthorship(done); + }); + + const clearAuthorship = (done) => { const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; diff --git a/tests/frontend/specs/ordered_list.js b/tests/frontend/specs/ordered_list.js index 7604e0287..7847f75ee 100644 --- a/tests/frontend/specs/ordered_list.js +++ b/tests/frontend/specs/ordered_list.js @@ -39,7 +39,8 @@ describe('assign ordered list', function () { it('does not insert unordered list', function (done) { this.timeout(3000); - helper.waitFor(() => helper.padInner$('div').first().find('ol li').length === 1).done(() => { + helper.waitFor( + () => helper.padInner$('div').first().find('ol li').length === 1).done(() => { expect().fail(() => 'Unordered list inserted, should ignore shortcut'); }).fail(() => { done(); @@ -69,7 +70,8 @@ describe('assign ordered list', function () { it('does not insert unordered list', function (done) { this.timeout(3000); - helper.waitFor(() => helper.padInner$('div').first().find('ol li').length === 1).done(() => { + helper.waitFor( + () => helper.padInner$('div').first().find('ol li').length === 1).done(() => { expect().fail(() => 'Unordered list inserted, should ignore shortcut'); }).fail(() => { done(); @@ -78,7 +80,8 @@ describe('assign ordered list', function () { }); }); - xit('issue #1125 keeps the numbered list on enter for the new line - EMULATES PASTING INTO A PAD', function (done) { + xit('issue #1125 keeps the numbered list on enter for the new line', function (done) { + // EMULATES PASTING INTO A PAD const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -104,19 +107,19 @@ describe('assign ordered list', function () { }); }); - const triggerCtrlShiftShortcut = function (shortcutChar) { + const triggerCtrlShiftShortcut = (shortcutChar) => { const inner$ = helper.padInner$; - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.ctrlKey = true; e.shiftKey = true; e.which = shortcutChar.toString().charCodeAt(0); inner$('#innerdocbody').trigger(e); }; - const makeSureShortcutIsDisabled = function (shortcut) { + const makeSureShortcutIsDisabled = (shortcut) => { helper.padChrome$.window.clientVars.padShortcutEnabled[shortcut] = false; }; - const makeSureShortcutIsEnabled = function (shortcut) { + const makeSureShortcutIsEnabled = (shortcut) => { helper.padChrome$.window.clientVars.padShortcutEnabled[shortcut] = true; }; }); @@ -142,7 +145,7 @@ describe('Pressing Tab in an OL increases and decreases indentation', function ( const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist'); $insertorderedlistButton.click(); - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.keyCode = 9; // tab inner$('#innerdocbody').trigger(e); @@ -156,7 +159,8 @@ describe('Pressing Tab in an OL increases and decreases indentation', function ( }); -describe('Pressing indent/outdent button in an OL increases and decreases indentation and bullet / ol formatting', function () { +describe('Pressing indent/outdent button in an OL increases and ' + + 'decreases indentation and bullet / ol formatting', function () { // create a new pad before each test run beforeEach(function (cb) { helper.newPad(cb); diff --git a/tests/frontend/specs/pad_modal.js b/tests/frontend/specs/pad_modal.js index 39c41a447..4e9c844b7 100644 --- a/tests/frontend/specs/pad_modal.js +++ b/tests/frontend/specs/pad_modal.js @@ -100,17 +100,17 @@ describe('Pad modal', function () { }); }); - const clickOnPadInner = function () { + const clickOnPadInner = () => { const $editor = helper.padInner$('#innerdocbody'); $editor.click(); }; - const clickOnPadOuter = function () { + const clickOnPadOuter = () => { const $lineNumbersColumn = helper.padOuter$('#sidedivinner'); $lineNumbersColumn.click(); }; - const openSettingsAndWaitForModalToBeVisible = function (done) { + const openSettingsAndWaitForModalToBeVisible = (done) => { helper.padChrome$('.buttonicon-settings').click(); // wait for modal to be displayed @@ -118,7 +118,7 @@ describe('Pad modal', function () { helper.waitFor(() => isModalOpened(modalSelector), 10000).done(done); }; - const isEditorDisabled = function () { + const isEditorDisabled = () => { const editorDocument = helper.padOuter$("iframe[name='ace_inner']").get(0).contentDocument; const editorBody = editorDocument.getElementById('innerdocbody'); @@ -128,7 +128,7 @@ describe('Pad modal', function () { return editorIsDisabled; }; - const isModalOpened = function (modalSelector) { + const isModalOpened = (modalSelector) => { const $modal = helper.padChrome$(modalSelector); return $modal.hasClass('popup-show'); diff --git a/tests/frontend/specs/responsiveness.js b/tests/frontend/specs/responsiveness.js index 090826979..ec63faa10 100644 --- a/tests/frontend/specs/responsiveness.js +++ b/tests/frontend/specs/responsiveness.js @@ -3,14 +3,17 @@ // Test for https://github.com/ether/etherpad-lite/issues/1763 // This test fails in Opera, IE and Safari -// Opera fails due to a weird way of handling the order of execution, yet actual performance seems fine +// Opera fails due to a weird way of handling the order of execution, +// yet actual performance seems fine // Safari fails due the delay being too great yet the actual performance seems fine // Firefox might panic that the script is taking too long so will fail // IE will fail due to running out of memory as it can't fit 2M chars in memory. -// Just FYI Google Docs crashes on large docs whilst trying to Save, it's likely the limitations we are +// Just FYI Google Docs crashes on large docs whilst trying to Save, +// it's likely the limitations we are // experiencing are more to do with browser limitations than improper implementation. -// A ueber fix for this would be to have a separate lower cpu priority thread that handles operations that aren't +// A ueber fix for this would be to have a separate lower cpu priority +// thread that handles operations that aren't // visible to the user. // Adapted from John McLear's original test case. @@ -22,16 +25,18 @@ xdescribe('Responsiveness of Editor', function () { this.timeout(6000); }); // JM commented out on 8th Sep 2020 for a release, after release this needs uncommenting - // And the test needs to be fixed to work in Firefox 52 on Windows 7. I am not sure why it fails on this specific platform - // The errors show this.timeout... then crash the browser but I am sure something is actually causing the stack trace and + // And the test needs to be fixed to work in Firefox 52 on Windows 7. + // I am not sure why it fails on this specific platform + // The errors show this.timeout... then crash the browser but + // I am sure something is actually causing the stack trace and // I just need to narrow down what, offers to help accepted. it('Fast response to keypress in pad with large amount of contents', function (done) { // skip on Windows Firefox 52.0 - if (window.bowser && window.bowser.windows && window.bowser.firefox && window.bowser.version == '52.0') { + if (window.bowser && + window.bowser.windows && window.bowser.firefox && window.bowser.version === '52.0') { this.skip(); } const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; const chars = '0000000000'; // row of placeholder chars const amount = 200000; // number of blocks of chars we will insert const length = (amount * (chars.length) + 1); // include a counter for each space @@ -41,7 +46,7 @@ xdescribe('Responsiveness of Editor', function () { // get keys to send const keyMultiplier = 10; // multiplier * 10 == total number of key events let keysToSend = ''; - for (var i = 0; i <= keyMultiplier; i++) { + for (let i = 0; i <= keyMultiplier; i++) { keysToSend += chars; } @@ -49,23 +54,23 @@ xdescribe('Responsiveness of Editor', function () { textElement.sendkeys('{selectall}'); // select all textElement.sendkeys('{del}'); // clear the pad text - for (var i = 0; i <= amount; i++) { + for (let i = 0; i <= amount; i++) { text = `${text + chars} `; // add the chars and space to the text contents } inner$('div').first().text(text); // Put the text contents into the pad - helper.waitFor(() => // Wait for the new contents to be on the pad - inner$('div').text().length > length - ).done(() => { - expect(inner$('div').text().length).to.be.greaterThan(length); // has the text changed? + // Wait for the new contents to be on the pad + helper.waitFor(() => inner$('div').text().length > length).done(() => { + // has the text changed? + expect(inner$('div').text().length).to.be.greaterThan(length); const start = Date.now(); // get the start time // send some new text to the screen (ensure all 3 key events are sent) const el = inner$('div').first(); for (let i = 0; i < keysToSend.length; ++i) { - var x = keysToSend.charCodeAt(i); + const x = keysToSend.charCodeAt(i); ['keyup', 'keypress', 'keydown'].forEach((type) => { - const e = $.Event(type); + const e = new $.Event(type); e.keyCode = x; el.trigger(e); }); diff --git a/tests/frontend/specs/select_formatting_buttons.js b/tests/frontend/specs/select_formatting_buttons.js index 666e81449..27b750e7f 100644 --- a/tests/frontend/specs/select_formatting_buttons.js +++ b/tests/frontend/specs/select_formatting_buttons.js @@ -91,7 +91,7 @@ describe('select formatting buttons when selection has style applied', function // select this text element $firstTextElement.sendkeys('{selectall}'); - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = key.charCodeAt(0); // I, U, B, 5 inner$('#innerdocbody').trigger(e); diff --git a/tests/frontend/specs/strikethrough.js b/tests/frontend/specs/strikethrough.js index bb477e01a..301aef64b 100644 --- a/tests/frontend/specs/strikethrough.js +++ b/tests/frontend/specs/strikethrough.js @@ -22,8 +22,6 @@ describe('strikethrough button', function () { const $strikethroughButton = chrome$('.buttonicon-strikethrough'); $strikethroughButton.click(); - // ace creates a new dom element when you press a button - // so just get the first text element again const $newFirstTextElement = inner$('div').first(); // is there a element now? diff --git a/tests/frontend/specs/timeslider.js b/tests/frontend/specs/timeslider.js index 401a71758..10f94b3cb 100644 --- a/tests/frontend/specs/timeslider.js +++ b/tests/frontend/specs/timeslider.js @@ -14,7 +14,6 @@ xdescribe('timeslider button takes you to the timeslider of a pad', function () // get the first text element inside the editable space const $firstTextElement = inner$('div span').first(); const originalValue = $firstTextElement.text(); // get the original value - const newValue = `Testing${originalValue}`; $firstTextElement.sendkeys('Testing'); // send line 1 to the pad const modifiedValue = $firstTextElement.text(); // get the modified value diff --git a/tests/frontend/specs/timeslider_labels.js b/tests/frontend/specs/timeslider_labels.js index 72a24256b..54e5da258 100644 --- a/tests/frontend/specs/timeslider_labels.js +++ b/tests/frontend/specs/timeslider_labels.js @@ -9,8 +9,10 @@ describe('timeslider', function () { /** * @todo test authorsList */ + it('Shows a correctly formatted date and time', async function () { this.timeout(12000); + // make some changes to produce 3 revisions const revs = 3; diff --git a/tests/frontend/specs/timeslider_revisions.js b/tests/frontend/specs/timeslider_revisions.js index de3c52a6f..4c2984686 100644 --- a/tests/frontend/specs/timeslider_revisions.js +++ b/tests/frontend/specs/timeslider_revisions.js @@ -26,8 +26,8 @@ describe('timeslider', function () { setTimeout(() => { // go to timeslider - $('#iframe-container iframe') - .attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider`); + $('#iframe-container iframe').attr('src', + `${$('#iframe-container iframe').attr('src')}/timeslider`); setTimeout(() => { const timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; @@ -84,8 +84,8 @@ describe('timeslider', function () { setTimeout(() => { // go to timeslider - $('#iframe-container iframe') - .attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider`); + $('#iframe-container iframe').attr('src', + `${$('#iframe-container iframe').attr('src')}/timeslider`); setTimeout(() => { const timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; @@ -102,15 +102,16 @@ describe('timeslider', function () { helper.waitFor( () => $('#iframe-container iframe')[0].contentWindow.location.hash !== oldUrl, 6000) .always(() => { - expect($('#iframe-container iframe')[0].contentWindow.location.hash) - .not.to.eql(oldUrl); + expect( + $('#iframe-container iframe')[0].contentWindow.location.hash + ).not.to.eql(oldUrl); done(); }); }, 6000); }, revs * timePerRev); }); it('jumps to a revision given in the url', function (done) { - this.timeout(6000); + this.timeout(40000); const inner$ = helper.padInner$; // wait for the text to be loaded @@ -132,15 +133,16 @@ describe('timeslider', function () { return lenOkay && colorOkay; }, 10000).always(() => { // go to timeslider with a specific revision set - $('#iframe-container iframe') - .attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider#0`); + + $('#iframe-container iframe').attr('src', + `${$('#iframe-container iframe').attr('src')}/timeslider#0`); // wait for the timeslider to be loaded helper.waitFor(() => { try { timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; } catch (e) { - // silently fart. Deadly. + // Empty catch block <3 } if (timeslider$) { return timeslider$('#innerdocbody').text().length === oldLength; @@ -161,8 +163,8 @@ describe('timeslider', function () { setTimeout(() => { // go to timeslider - $('#iframe-container iframe') - .attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider#0`); + $('#iframe-container iframe').attr('src', + `${$('#iframe-container iframe').attr('src')}/timeslider#0`); let timeslider$; let exportLink; @@ -170,7 +172,7 @@ describe('timeslider', function () { try { timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; } catch (e) { - // silently give up on life. + // Empty catch block <3 } if (!timeslider$) return false; exportLink = timeslider$('#exportplaina').attr('href'); diff --git a/tests/frontend/specs/undo.js b/tests/frontend/specs/undo.js index eabc4005d..95936669d 100644 --- a/tests/frontend/specs/undo.js +++ b/tests/frontend/specs/undo.js @@ -43,7 +43,7 @@ describe('undo button', function () { const modifiedValue = $firstTextElement.text(); // get the modified value expect(modifiedValue).not.to.be(originalValue); // expect the value to change - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 90; // z inner$('#innerdocbody').trigger(e); diff --git a/tests/frontend/specs/unordered_list.js b/tests/frontend/specs/unordered_list.js index af13af2b2..45ed4ad41 100644 --- a/tests/frontend/specs/unordered_list.js +++ b/tests/frontend/specs/unordered_list.js @@ -138,7 +138,8 @@ describe('Pressing Tab in an UL increases and decreases indentation', function ( }); }); -describe('Pressing indent/outdent button in an UL increases and decreases indentation and bullet / ol formatting', function () { +describe('Pressing indent/outdent button in an UL increases and decreases indentation ' + + 'and bullet / ol formatting', function () { // create a new pad before each test run beforeEach(function (cb) { helper.newPad(cb); diff --git a/tests/frontend/travis/remote_runner.js b/tests/frontend/travis/remote_runner.js index 70c850ca8..ba0e7be0c 100644 --- a/tests/frontend/travis/remote_runner.js +++ b/tests/frontend/travis/remote_runner.js @@ -1,17 +1,19 @@ -var srcFolder = '../../../src/node_modules/'; -var wd = require(`${srcFolder}wd`); -var async = require(`${srcFolder}async`); +'use strict'; -var config = { +const wd = require('ep_etherpad-lite/node_modules/wd'); +const async = require('ep_etherpad-lite/node_modules/async'); + +const config = { host: 'ondemand.saucelabs.com', port: 80, username: process.env.SAUCE_USER, accessKey: process.env.SAUCE_ACCESS_KEY, }; -var allTestsPassed = true; +let allTestsPassed = true; // overwrite the default exit code -// in case not all worker can be run (due to saucelabs limits), `queue.drain` below will not be called +// in case not all worker can be run (due to saucelabs limits), +// `queue.drain` below will not be called // and the script would silently exit with error code 0 process.exitCode = 2; process.on('exit', (code) => { @@ -20,13 +22,18 @@ process.on('exit', (code) => { } }); -var sauceTestWorker = async.queue((testSettings, callback) => { - const browser = wd.promiseChainRemote(config.host, config.port, config.username, config.accessKey); - const name = `${process.env.GIT_HASH} - ${testSettings.browserName} ${testSettings.version}, ${testSettings.platform}`; +const sauceTestWorker = async.queue((testSettings, callback) => { + const browser = wd.promiseChainRemote( + config.host, config.port, config.username, config.accessKey); + const name = + `${process.env.GIT_HASH} - ${testSettings.browserName} ` + + `${testSettings.version}, ${testSettings.platform}`; testSettings.name = name; testSettings.public = true; testSettings.build = process.env.GIT_HASH; - testSettings.extendedDebugging = true; // console.json can be downloaded via saucelabs, don't know how to print them into output of the tests + // console.json can be downloaded via saucelabs, + // don't know how to print them into output of the tests + testSettings.extendedDebugging = true; testSettings.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; browser.init(testSettings).get('http://localhost:9001/tests/frontend/', () => { @@ -34,7 +41,7 @@ var sauceTestWorker = async.queue((testSettings, callback) => { console.log(`Remote sauce test '${name}' started! ${url}`); // tear down the test excecution - const stopSauce = function (success, timesup) { + const stopSauce = (success, timesup) => { clearInterval(getStatusInterval); clearTimeout(timeout); @@ -43,12 +50,15 @@ var sauceTestWorker = async.queue((testSettings, callback) => { allTestsPassed = false; } - // if stopSauce is called via timeout (in contrast to via getStatusInterval) than the log of up to the last + // if stopSauce is called via timeout + // (in contrast to via getStatusInterval) than the log of up to the last // five seconds may not be available here. It's an error anyway, so don't care about it. printLog(logIndex); if (timesup) { - console.log(`[${testSettings.browserName} ${testSettings.platform}${testSettings.version === '' ? '' : (` ${testSettings.version}`)}] \x1B[31mFAILED\x1B[39m allowed test duration exceeded`); + console.log(`[${testSettings.browserName} ${testSettings.platform}` + + `${testSettings.version === '' ? '' : (` ${testSettings.version}`)}]` + + ' \x1B[31mFAILED\x1B[39m allowed test duration exceeded'); } console.log(`Remote sauce test '${name}' finished! ${url}`); @@ -58,17 +68,19 @@ var sauceTestWorker = async.queue((testSettings, callback) => { /** * timeout if a test hangs or the job exceeds 14.5 minutes - * It's necessary because if travis kills the saucelabs session due to inactivity, we don't get any output - * @todo this should be configured in testSettings, see https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-Timeouts + * It's necessary because if travis kills the saucelabs session due to inactivity, + * we don't get any output + * @todo this should be configured in testSettings, see + * https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-Timeouts */ - var timeout = setTimeout(() => { + const timeout = setTimeout(() => { stopSauce(false, true); }, 870000); // travis timeout is 15 minutes, set this to a slightly lower value let knownConsoleText = ''; // how many characters of the log have been sent to travis let logIndex = 0; - var getStatusInterval = setInterval(() => { + const getStatusInterval = setInterval(() => { browser.eval("$('#console').text()", (err, consoleText) => { if (!consoleText || err) { return; @@ -76,9 +88,10 @@ var sauceTestWorker = async.queue((testSettings, callback) => { knownConsoleText = consoleText; if (knownConsoleText.indexOf('FINISHED') > 0) { - const match = knownConsoleText.match(/FINISHED.*([0-9]+) tests passed, ([0-9]+) tests failed/); + const match = knownConsoleText.match( + /FINISHED.*([0-9]+) tests passed, ([0-9]+) tests failed/); // finished without failures - if (match[2] && match[2] == '0') { + if (match[2] && match[2] === '0') { stopSauce(true); // finished but some tests did not return or some tests failed @@ -99,13 +112,17 @@ var sauceTestWorker = async.queue((testSettings, callback) => { * * @param {number} index offset from where to start */ - function printLog(index) { - let testResult = knownConsoleText.substring(index).replace(/\[red\]/g, '\x1B[31m').replace(/\[yellow\]/g, '\x1B[33m') + const printLog = (index) => { + let testResult = knownConsoleText.substring(index) + .replace(/\[red\]/g, '\x1B[31m').replace(/\[yellow\]/g, '\x1B[33m') .replace(/\[green\]/g, '\x1B[32m').replace(/\[clear\]/g, '\x1B[39m'); - testResult = testResult.split('\\n').map((line) => `[${testSettings.browserName} ${testSettings.platform}${testSettings.version === '' ? '' : (` ${testSettings.version}`)}] ${line}`).join('\n'); + testResult = testResult.split('\\n').map((line) => `[${testSettings.browserName} ` + + `${testSettings.platform}` + + `${testSettings.version === '' ? '' : (` ${testSettings.version}`)}]` + + `${line}`).join('\n'); console.log(testResult); - } + }; }); }, 6); // run 6 tests in parrallel diff --git a/tests/ratelimit/send_changesets.js b/tests/ratelimit/send_changesets.js index b0d994c8c..92af23e18 100644 --- a/tests/ratelimit/send_changesets.js +++ b/tests/ratelimit/send_changesets.js @@ -1,8 +1,12 @@ +'use strict'; + +let etherpad; try { - var etherpad = require('../../src/node_modules/etherpad-cli-client'); + etherpad = require('ep_etherpad-lite/node_modules/etherpad-cli-client'); // ugly } catch { - var etherpad = require('etherpad-cli-client'); + /* eslint-disable-next-line node/no-missing-require */ + etherpad = require('etherpad-cli-client'); // uses global } const pad = etherpad.connect(process.argv[2]); pad.on('connected', () => { @@ -18,7 +22,7 @@ pad.on('connected', () => { }); // in case of disconnect exit code 1 pad.on('message', (message) => { - if (message.disconnect == 'rateLimited') { + if (message.disconnect === 'rateLimited') { process.exit(1); } });