Merge branch 'develop' into mochawesome

This commit is contained in:
John McLear 2021-02-03 11:23:54 +00:00 committed by GitHub
commit 7630e71957
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 1347 additions and 1020 deletions

View file

@ -1,4 +1,8 @@
# Changes for the next release # Changes for the next release
### Compatibility changes
* Node.js 10.17.0 or newer is now required.
### Notable new features ### Notable new features
* Database performance is significantly improved. * Database performance is significantly improved.
@ -118,7 +122,7 @@
* MINOR: Fix ?showChat URL param issue * MINOR: Fix ?showChat URL param issue
* MINOR: Issue where timeslider URI fails to be correct if padID is numeric * 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 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: Several Colibris CSS fixes
* MINOR: Use mime library for mime types instead of hard-coded. * MINOR: Use mime library for mime types instead of hard-coded.
* MINOR: Don't show "new pad button" if instance is read only * 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 # 1.5.3
* NEW: Accessibility support for Screen readers, includes new fonts and keyboard shortcuts * NEW: Accessibility support for Screen readers, includes new fonts and keyboard shortcuts
* NEW: API endpoint for Append Chat Message and Chat Backend Tests * 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: Content Collector can handle key values
* NEW: getAttributesOnPosition Method * NEW: getAttributesOnPosition Method
* FIX: Firefox keeps attributes (bold etc) on cut/copy -> paste * 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: Timeslider UI Fix
* Fix: Remove Dokuwiki * Fix: Remove Dokuwiki
* Fix: Remove long paths from windows build (stops error during extract) * 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: Move all scripts into bin/
* Fix: Various CSS bugfixes for Mobile devices * Fix: Various CSS bugfixes for Mobile devices
* Fix: Overflow Toolbar * 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: 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: check if uploaded file only contains ascii chars when abiword disabled
* FIX: Plugin search in /admin/plugins * 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: Drop messages from unknown connections (would lead to a crash after a restart)
* FIX: API: fix createGroupFor endpoint, if mapped group is deleted * FIX: API: fix createGroupFor endpoint, if mapped group is deleted
* FIX: Import form for other locales * 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 * NEW: Bump log4js for improved logging
* Fix: Remove URL schemes which don't have RFC standard * Fix: Remove URL schemes which don't have RFC standard
* Fix: Fix safeRun subsequent restarts issue * 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: Include script for more efficient import
* Fix: Fix sysv comptibile script * Fix: Fix sysv comptibile script
* Fix: Fix client side changeset spamming * 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: Support Node 0.10
* Fix: Log HTTP on DEBUG log level * Fix: Log HTTP on DEBUG log level
* Fix: Server wont crash on import fails on 0 file import. * 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: Language support for non existing languages
* Fix: Mobile support for chat notifications are now usable * Fix: Mobile support for chat notifications are now usable
* Fix: Re-Enable Editbar buttons on reconnect * 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: Admin dashboard mobile device support and new hooks for Admin dashboard
* NEW: Get current API version from API * NEW: Get current API version from API
* NEW: CLI script to delete pads * 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: Text Export indentation now supports multiple indentations
* Fix: Bugfix getChatHistory API method * Fix: Bugfix getChatHistory API method
* Fix: Stop Chrome losing caret after paste is texted * 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 Opera browser inserting two new lines on enter keypress
* Fix: Stop timeslider from showing NaN on pads with only one revision * 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: 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: Update to latest jQuery
* Other: Change loading message asking user to please wait on first build * 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) * Other: Allow etherpad to use global npm installation (Safe since node 6.3)

View file

@ -13,7 +13,7 @@ Etherpad is a real-time collaborative editor [scalable to thousands of simultane
# Installation # Installation
## Requirements ## Requirements
- `nodejs` >= **10.13.0**. - `nodejs` >= **10.17.0**.
## GNU/Linux and other UNIX-like systems ## 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 ### 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):** **As any user (we recommend creating a separate user called etherpad):**

View file

@ -51,7 +51,7 @@ const util = require('util');
let atext = Changeset.makeAText('\n'); let atext = Changeset.makeAText('\n');
// run trough all revisions // run through all revisions
for (const revNum of revisions) { for (const revNum of revisions) {
// console.log('Fetching', revNum) // console.log('Fetching', revNum)
const revision = await db.get(`pad:${padId}:revs:${revNum}`); const revision = await db.get(`pad:${padId}:revs:${revNum}`);

View file

@ -3,7 +3,7 @@
# Move to the folder where ep-lite is installed # Move to the folder where ep-lite is installed
cd "$(dirname "$0")"/.. cd "$(dirname "$0")"/..
# Source constants and usefull functions # Source constants and useful functions
. bin/functions.sh . bin/functions.sh
#Was this script started in the bin folder? if yes move out #Was this script started in the bin folder? if yes move out

View file

@ -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;
}
}

View file

@ -3,7 +3,7 @@
# Move to the folder where ep-lite is installed # Move to the folder where ep-lite is installed
cd "$(dirname "$0")"/.. cd "$(dirname "$0")"/..
# Source constants and usefull functions # Source constants and useful functions
. bin/functions.sh . bin/functions.sh
# Prepare the environment # Prepare the environment

View file

@ -1,4 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict';
// Copyright Joyent, Inc. and other Node contributors. // Copyright Joyent, Inc. and other Node contributors.
// //
// Permission is hereby granted, free of charge, to any person obtaining a // 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 // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE. // USE OR OTHER DEALINGS IN THE SOFTWARE.
const marked = require('marked');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
@ -33,12 +35,12 @@ let template = null;
let inputFile = null; let inputFile = null;
args.forEach((arg) => { args.forEach((arg) => {
if (!arg.match(/^\-\-/)) { if (!arg.match(/^--/)) {
inputFile = arg; inputFile = arg;
} else if (arg.match(/^\-\-format=/)) { } else if (arg.match(/^--format=/)) {
format = arg.replace(/^\-\-format=/, ''); format = arg.replace(/^--format=/, '');
} else if (arg.match(/^\-\-template=/)) { } else if (arg.match(/^--template=/)) {
template = arg.replace(/^\-\-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 = {}; const includeData = {};
function processIncludes(inputFile, input, cb) { const processIncludes = (inputFile, input, cb) => {
const includes = input.match(includeExpr); const includes = input.match(includeExpr);
if (includes === null) return cb(null, input); if (includes == null) return cb(null, input);
let errState = null; let errState = null;
console.error(includes); console.error(includes);
let incCount = includes.length; let incCount = includes.length;
@ -70,7 +72,7 @@ function processIncludes(inputFile, input, cb) {
let fname = include.replace(/^@include\s+/, ''); let fname = include.replace(/^@include\s+/, '');
if (!fname.match(/\.md$/)) fname += '.md'; if (!fname.match(/\.md$/)) fname += '.md';
if (includeData.hasOwnProperty(fname)) { if (Object.prototype.hasOwnProperty.call(includeData, fname)) {
input = input.split(include).join(includeData[fname]); input = input.split(include).join(includeData[fname]);
incCount--; incCount--;
if (incCount === 0) { if (incCount === 0) {
@ -94,10 +96,10 @@ function processIncludes(inputFile, input, cb) {
}); });
}); });
}); });
} };
function next(er, input) { const next = (er, input) => {
if (er) throw er; if (er) throw er;
switch (format) { switch (format) {
case 'json': case 'json':
@ -117,4 +119,4 @@ function next(er, input) {
default: default:
throw new Error(`Invalid format: ${format}`); throw new Error(`Invalid format: ${format}`);
} }
} };

View file

@ -1,3 +1,5 @@
'use strict';
// Copyright Joyent, Inc. and other Node contributors. // Copyright Joyent, Inc. and other Node contributors.
// //
// Permission is hereby granted, free of charge, to any person obtaining a // 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 marked = require('marked');
const path = require('path'); const path = require('path');
module.exports = toHTML;
function toHTML(input, filename, template, cb) { const toHTML = (input, filename, template, cb) => {
const lexed = marked.lexer(input); const lexed = marked.lexer(input);
fs.readFile(template, 'utf8', (er, template) => { fs.readFile(template, 'utf8', (er, template) => {
if (er) return cb(er); if (er) return cb(er);
render(lexed, filename, template, cb); render(lexed, filename, template, cb);
}); });
} };
module.exports = toHTML;
function render(lexed, filename, template, cb) { const render = (lexed, filename, template, cb) => {
// get the section // get the section
const section = getSection(lexed); const section = getSection(lexed);
@ -52,23 +54,23 @@ function render(lexed, filename, template, cb) {
// content has to be the last thing we do with // content has to be the last thing we do with
// the lexed tokens, because it's destructive. // the lexed tokens, because it's destructive.
content = marked.parser(lexed); const content = marked.parser(lexed);
template = template.replace(/__CONTENT__/g, content); template = template.replace(/__CONTENT__/g, content);
cb(null, template); cb(null, template);
}); });
} };
// just update the list item text in-place. // just update the list item text in-place.
// lists that come right after a heading are what we're after. // lists that come right after a heading are what we're after.
function parseLists(input) { const parseLists = (input) => {
let state = null; let state = null;
let depth = 0; let depth = 0;
const output = []; const output = [];
output.links = input.links; output.links = input.links;
input.forEach((tok) => { input.forEach((tok) => {
if (state === null) { if (state == null) {
if (tok.type === 'heading') { if (tok.type === 'heading') {
state = 'AFTERHEADING'; state = 'AFTERHEADING';
} }
@ -112,29 +114,27 @@ function parseLists(input) {
}); });
return output; return output;
} };
function parseListItem(text) { const parseListItem = (text) => {
text = text.replace(/\{([^\}]+)\}/, '<span class="type">$1</span>'); text = text.replace(/\{([^}]+)\}/, '<span class="type">$1</span>');
// XXX maybe put more stuff here? // XXX maybe put more stuff here?
return text; return text;
} };
// section is just the first heading // section is just the first heading
function getSection(lexed) { const getSection = (lexed) => {
const section = '';
for (let i = 0, l = lexed.length; i < l; i++) { for (let i = 0, l = lexed.length; i < l; i++) {
const tok = lexed[i]; const tok = lexed[i];
if (tok.type === 'heading') return tok.text; if (tok.type === 'heading') return tok.text;
} }
return ''; return '';
} };
function buildToc(lexed, filename, cb) { const buildToc = (lexed, filename, cb) => {
const indent = 0;
let toc = []; let toc = [];
let depth = 0; let depth = 0;
lexed.forEach((tok) => { lexed.forEach((tok) => {
@ -155,18 +155,18 @@ function buildToc(lexed, filename, cb) {
toc = marked.parse(toc.join('\n')); toc = marked.parse(toc.join('\n'));
cb(null, toc); cb(null, toc);
} };
const idCounters = {}; const idCounters = {};
function getId(text) { const getId = (text) => {
text = text.toLowerCase(); text = text.toLowerCase();
text = text.replace(/[^a-z0-9]+/g, '_'); text = text.replace(/[^a-z0-9]+/g, '_');
text = text.replace(/^_+|_+$/, ''); text = text.replace(/^_+|_+$/, '');
text = text.replace(/^([^a-z])/, '_$1'); text = text.replace(/^([^a-z])/, '_$1');
if (idCounters.hasOwnProperty(text)) { if (Object.prototype.hasOwnProperty.call(idCounters, text)) {
text += `_${++idCounters[text]}`; text += `_${++idCounters[text]}`;
} else { } else {
idCounters[text] = 0; idCounters[text] = 0;
} }
return text; return text;
} };

View file

@ -1,3 +1,4 @@
'use strict';
// Copyright Joyent, Inc. and other Node contributors. // Copyright Joyent, Inc. and other Node contributors.
// //
// Permission is hereby granted, free of charge, to any person obtaining a // Permission is hereby granted, free of charge, to any person obtaining a
@ -26,7 +27,7 @@ module.exports = doJSON;
const marked = require('marked'); const marked = require('marked');
function doJSON(input, filename, cb) { const doJSON = (input, filename, cb) => {
const root = {source: filename}; const root = {source: filename};
const stack = [root]; const stack = [root];
let depth = 0; let depth = 0;
@ -40,7 +41,7 @@ function doJSON(input, filename, cb) {
// <!-- type = module --> // <!-- type = module -->
// This is for cases where the markdown semantic structure is lacking. // This is for cases where the markdown semantic structure is lacking.
if (type === 'paragraph' || type === 'html') { if (type === 'paragraph' || type === 'html') {
const metaExpr = /<!--([^=]+)=([^\-]+)-->\n*/g; const metaExpr = /<!--([^=]+)=([^-]+)-->\n*/g;
text = text.replace(metaExpr, (_0, k, v) => { text = text.replace(metaExpr, (_0, k, v) => {
current[k.trim()] = v.trim(); current[k.trim()] = v.trim();
return ''; return '';
@ -146,7 +147,7 @@ function doJSON(input, filename, cb) {
} }
return cb(null, root); return cb(null, root);
} };
// go from something like this: // 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.', // desc: 'whether or not to send output to parent\'s stdio.',
// default: 'false' } ] } ] // default: 'false' } ] } ]
function processList(section) { const processList = (section) => {
const list = section.list; const list = section.list;
const values = []; const values = [];
let current; let current;
@ -203,13 +204,13 @@ function processList(section) {
if (type === 'space') return; if (type === 'space') return;
if (type === 'list_item_start') { if (type === 'list_item_start') {
if (!current) { if (!current) {
var n = {}; const n = {};
values.push(n); values.push(n);
current = n; current = n;
} else { } else {
current.options = current.options || []; current.options = current.options || [];
stack.push(current); stack.push(current);
var n = {}; const n = {};
current.options.push(n); current.options.push(n);
current = n; current = n;
} }
@ -247,11 +248,11 @@ function processList(section) {
switch (section.type) { switch (section.type) {
case 'ctor': case 'ctor':
case 'classMethod': case 'classMethod':
case 'method': case 'method': {
// each item is an argument, unless the name is 'return', // each item is an argument, unless the name is 'return',
// in which case it's the return value. // in which case it's the return value.
section.signatures = section.signatures || []; section.signatures = section.signatures || [];
var sig = {}; const sig = {};
section.signatures.push(sig); section.signatures.push(sig);
sig.params = values.filter((v) => { sig.params = values.filter((v) => {
if (v.name === 'return') { if (v.name === 'return') {
@ -262,11 +263,11 @@ function processList(section) {
}); });
parseSignature(section.textRaw, sig); parseSignature(section.textRaw, sig);
break; break;
}
case 'property': case 'property': {
// there should be only one item, which is the value. // there should be only one item, which is the value.
// copy the data up to the section. // copy the data up to the section.
var value = values[0] || {}; const value = values[0] || {};
delete value.name; delete value.name;
section.typeof = value.type; section.typeof = value.type;
delete value.type; delete value.type;
@ -274,20 +275,21 @@ function processList(section) {
section[k] = value[k]; section[k] = value[k];
}); });
break; break;
}
case 'event': case 'event': {
// event: each item is an argument. // event: each item is an argument.
section.params = values; section.params = values;
break; break;
} }
// section.listParsed = values;
delete section.list;
} }
delete section.list;
};
// textRaw = "someobject.someMethod(a, [b=100], [c])" // textRaw = "someobject.someMethod(a, [b=100], [c])"
function parseSignature(text, sig) { const parseSignature = (text, sig) => {
let params = text.match(paramExpr); let params = text.match(paramExpr);
if (!params) return; if (!params) return;
params = params[1]; params = params[1];
@ -322,10 +324,10 @@ function parseSignature(text, sig) {
if (optional) param.optional = true; if (optional) param.optional = true;
if (def !== undefined) param.default = def; if (def !== undefined) param.default = def;
}); });
} };
function parseListItem(item) { const parseListItem = (item) => {
if (item.options) item.options.forEach(parseListItem); if (item.options) item.options.forEach(parseListItem);
if (!item.textRaw) return; if (!item.textRaw) return;
@ -341,7 +343,7 @@ function parseListItem(item) {
item.name = 'return'; item.name = 'return';
text = text.replace(retExpr, ''); text = text.replace(retExpr, '');
} else { } else {
const nameExpr = /^['`"]?([^'`": \{]+)['`"]?\s*:?\s*/; const nameExpr = /^['`"]?([^'`": {]+)['`"]?\s*:?\s*/;
const name = text.match(nameExpr); const name = text.match(nameExpr);
if (name) { if (name) {
item.name = name[1]; item.name = name[1];
@ -358,7 +360,7 @@ function parseListItem(item) {
} }
text = text.trim(); text = text.trim();
const typeExpr = /^\{([^\}]+)\}/; const typeExpr = /^\{([^}]+)\}/;
const type = text.match(typeExpr); const type = text.match(typeExpr);
if (type) { if (type) {
item.type = type[1]; item.type = type[1];
@ -376,10 +378,10 @@ function parseListItem(item) {
text = text.replace(/^\s*-\s*/, ''); text = text.replace(/^\s*-\s*/, '');
text = text.trim(); text = text.trim();
if (text) item.desc = text; if (text) item.desc = text;
} };
function finishSection(section, parent) { const finishSection = (section, parent) => {
if (!section || !parent) { if (!section || !parent) {
throw new Error(`Invalid finishSection call\n${ throw new Error(`Invalid finishSection call\n${
JSON.stringify(section)}\n${ JSON.stringify(section)}\n${
@ -416,7 +418,7 @@ function finishSection(section, parent) {
ctor.signatures.forEach((sig) => { ctor.signatures.forEach((sig) => {
sig.desc = ctor.desc; sig.desc = ctor.desc;
}); });
sigs.push.apply(sigs, ctor.signatures); sigs.push(...ctor.signatures);
}); });
delete section.ctors; delete section.ctors;
} }
@ -479,50 +481,50 @@ function finishSection(section, parent) {
parent[plur] = parent[plur] || []; parent[plur] = parent[plur] || [];
parent[plur].push(section); parent[plur].push(section);
} };
// Not a general purpose deep copy. // Not a general purpose deep copy.
// But sufficient for these basic things. // But sufficient for these basic things.
function deepCopy(src, dest) { const deepCopy = (src, dest) => {
Object.keys(src).filter((k) => !dest.hasOwnProperty(k)).forEach((k) => { Object.keys(src).filter((k) => !Object.prototype.hasOwnProperty.call(dest, k)).forEach((k) => {
dest[k] = deepCopy_(src[k]); dest[k] = deepCopy_(src[k]);
}); });
} };
function deepCopy_(src) { const deepCopy_ = (src) => {
if (!src) return src; if (!src) return src;
if (Array.isArray(src)) { if (Array.isArray(src)) {
var c = new Array(src.length); const c = new Array(src.length);
src.forEach((v, i) => { src.forEach((v, i) => {
c[i] = deepCopy_(v); c[i] = deepCopy_(v);
}); });
return c; return c;
} }
if (typeof src === 'object') { if (typeof src === 'object') {
var c = {}; const c = {};
Object.keys(src).forEach((k) => { Object.keys(src).forEach((k) => {
c[k] = deepCopy_(src[k]); c[k] = deepCopy_(src[k]);
}); });
return c; return c;
} }
return src; return src;
} };
// these parse out the contents of an H# tag // these parse out the contents of an H# tag
const eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i; const eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i;
const classExpr = /^Class:\s*([^ ]+).*?$/i; const classExpr = /^Class:\s*([^ ]+).*?$/i;
const propExpr = /^(?:property:?\s*)?[^\.]+\.([^ \.\(\)]+)\s*?$/i; const propExpr = /^(?:property:?\s*)?[^.]+\.([^ .()]+)\s*?$/i;
const braceExpr = /^(?:property:?\s*)?[^\.\[]+(\[[^\]]+\])\s*?$/i; const braceExpr = /^(?:property:?\s*)?[^.[]+(\[[^\]]+\])\s*?$/i;
const classMethExpr = const classMethExpr =
/^class\s*method\s*:?[^\.]+\.([^ \.\(\)]+)\([^\)]*\)\s*?$/i; /^class\s*method\s*:?[^.]+\.([^ .()]+)\([^)]*\)\s*?$/i;
const methExpr = const methExpr =
/^(?:method:?\s*)?(?:[^\.]+\.)?([^ \.\(\)]+)\([^\)]*\)\s*?$/i; /^(?:method:?\s*)?(?:[^.]+\.)?([^ .()]+)\([^)]*\)\s*?$/i;
const newExpr = /^new ([A-Z][a-z]+)\([^\)]*\)\s*?$/; const newExpr = /^new ([A-Z][a-z]+)\([^)]*\)\s*?$/;
var paramExpr = /\((.*)\);?$/; const paramExpr = /\((.*)\);?$/;
function newSection(tok) { const newSection = (tok) => {
const section = {}; const section = {};
// infer the type from the text. // infer the type from the text.
const text = section.textRaw = tok.text; const text = section.textRaw = tok.text;
@ -551,4 +553,4 @@ function newSection(tok) {
section.name = text; section.name = text;
} }
return section; return section;
} };

View file

@ -4,7 +4,7 @@
"description": "Internal tool for generating Node.js API docs", "description": "Internal tool for generating Node.js API docs",
"version": "0.0.0", "version": "0.0.0",
"engines": { "engines": {
"node": ">=0.6.10" "node": ">=10.17.0"
}, },
"dependencies": { "dependencies": {
"marked": "0.8.2" "marked": "0.8.2"

View file

@ -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 # 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 )" DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
# Source constants and usefull functions # Source constants and useful functions
. ${DIR}/../bin/functions.sh . ${DIR}/../bin/functions.sh
echo "Running directly, without checking/installing dependencies" echo "Running directly, without checking/installing dependencies"

View file

@ -3,7 +3,7 @@
# Move to the folder where ep-lite is installed # Move to the folder where ep-lite is installed
cd "$(dirname "$0")"/.. cd "$(dirname "$0")"/..
# Source constants and usefull functions # Source constants and useful functions
. bin/functions.sh . bin/functions.sh
# Is node installed? # Is node installed?

View file

@ -263,7 +263,7 @@ fs.readdir(pluginPath, (err, rootFiles) => {
console.warn('No engines or node engine in package.json'); console.warn('No engines or node engine in package.json');
if (autoFix) { if (autoFix) {
const engines = { const engines = {
node: '>=10.13.0', node: '^10.17.0 || >=11.14.0',
}; };
parsedPackageJSON.engines = engines; parsedPackageJSON.engines = engines;
writePackageJson(parsedPackageJSON); writePackageJson(parsedPackageJSON);

View file

@ -23,7 +23,7 @@ const util = require('util');
(async () => { (async () => {
await util.promisify(npm.load)({}); await util.promisify(npm.load)({});
// intialize database // initialize database
require('ep_etherpad-lite/node/utils/Settings'); require('ep_etherpad-lite/node/utils/Settings');
const db = require('ep_etherpad-lite/node/db/DB'); const db = require('ep_etherpad-lite/node/db/DB');
await db.init(); await db.init();

View file

@ -3,7 +3,7 @@
# Move to the folder where ep-lite is installed # Move to the folder where ep-lite is installed
cd "$(dirname "$0")"/.. cd "$(dirname "$0")"/..
# Source constants and usefull functions # Source constants and useful functions
. bin/functions.sh . bin/functions.sh
ignoreRoot=0 ignoreRoot=0

View file

@ -263,7 +263,7 @@ deletes a session
#### getSessionInfo(sessionID) #### getSessionInfo(sessionID)
* API >= 1 * API >= 1
returns informations about a session returns information about a session
*Example returns:* *Example returns:*
* `{code: 0, message:"ok", data: {authorID: "a.s8oes9dhwrvt0zif", groupID: g.s8oes9dhwrvt0zif, validUntil: 1312201246}}` * `{code: 0, message:"ok", data: {authorID: "a.s8oes9dhwrvt0zif", groupID: g.s8oes9dhwrvt0zif, validUntil: 1312201246}}`

View file

@ -95,7 +95,7 @@ For example, if you want to replace `Chat` with `Notes`, simply add...
## Customization for Administrators ## 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`: 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`:

View file

@ -225,7 +225,7 @@ publish your plugin.
"author": "USERNAME (REAL NAME) <MAIL@EXAMPLE.COM>", "author": "USERNAME (REAL NAME) <MAIL@EXAMPLE.COM>",
"contributors": [], "contributors": [],
"dependencies": {"MODULE": "0.3.20"}, "dependencies": {"MODULE": "0.3.20"},
"engines": { "node": ">= 10.13.0"} "engines": { "node": "^10.17.0 || >=11.14.0"}
} }
``` ```

View file

@ -39,7 +39,9 @@
"tests/**/*" "tests/**/*"
], ],
"excludedFiles": [ "excludedFiles": [
"**/.eslintrc.js" "**/.eslintrc.js",
"tests/frontend/travis/**/*",
"tests/ratelimit/**/*"
], ],
"extends": "etherpad/tests", "extends": "etherpad/tests",
"rules": { "rules": {
@ -75,7 +77,8 @@
"tests/frontend/**/*" "tests/frontend/**/*"
], ],
"excludedFiles": [ "excludedFiles": [
"**/.eslintrc.js" "**/.eslintrc.js",
"tests/frontend/travis/**/*"
], ],
"extends": "etherpad/tests/frontend", "extends": "etherpad/tests/frontend",
"overrides": [ "overrides": [
@ -92,6 +95,12 @@
} }
} }
] ]
},
{
"files": [
"tests/frontend/travis/**/*"
],
"extends": "etherpad/node"
} }
], ],
"root": true "root": true
@ -100,6 +109,6 @@
"lint": "eslint ." "lint": "eslint ."
}, },
"engines": { "engines": {
"node": ">=10.13.0" "node": "^10.17.0 || >=11.14.0"
} }
} }

View file

@ -171,7 +171,7 @@
* *
* *
* Database specific settings are dependent on dbType, and go in dbSettings. * 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. * credentials.json.
* *
* For a complete list of the supported drivers, please refer to: * For a complete list of the supported drivers, please refer to:

View file

@ -162,7 +162,7 @@
* *
* *
* Database specific settings are dependent on dbType, and go in dbSettings. * 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. * credentials.json.
* *
* For a complete list of the supported drivers, please refer to: * For a complete list of the supported drivers, please refer to:

View file

@ -2,12 +2,47 @@
"@metadata": { "@metadata": {
"authors": [ "authors": [
"Elisardojm", "Elisardojm",
"Ghose",
"Toliño" "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.newPad": "Novo documento",
"index.createOpenPad": "ou cree/abra un documento co nome:", "index.createOpenPad": "ou crea/abre un documento co nome:",
"pad.toolbar.bold.title": "Negra (Ctrl-B)", "index.openPad": "abrir un Pad existente co nome:",
"pad.toolbar.bold.title": "Resaltado (Ctrl-B)",
"pad.toolbar.italic.title": "Cursiva (Ctrl-I)", "pad.toolbar.italic.title": "Cursiva (Ctrl-I)",
"pad.toolbar.underline.title": "Subliñar (Ctrl-U)", "pad.toolbar.underline.title": "Subliñar (Ctrl-U)",
"pad.toolbar.strikethrough.title": "Riscar (Ctrl+5)", "pad.toolbar.strikethrough.title": "Riscar (Ctrl+5)",
@ -17,28 +52,30 @@
"pad.toolbar.unindent.title": "Sen sangría (Maiús.+TAB)", "pad.toolbar.unindent.title": "Sen sangría (Maiús.+TAB)",
"pad.toolbar.undo.title": "Desfacer (Ctrl-Z)", "pad.toolbar.undo.title": "Desfacer (Ctrl-Z)",
"pad.toolbar.redo.title": "Refacer (Ctrl-Y)", "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.import_export.title": "Importar/Exportar desde/a diferentes formatos de ficheiro",
"pad.toolbar.timeslider.title": "Liña do tempo", "pad.toolbar.timeslider.title": "Liña do tempo",
"pad.toolbar.savedRevision.title": "Gardar a revisión", "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.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.save": "Gardar",
"pad.colorpicker.cancel": "Cancelar", "pad.colorpicker.cancel": "Cancelar",
"pad.loading": "Cargando...", "pad.loading": "Cargando...",
"pad.noCookie": "Non se puido atopar a cookie. Por favor, habilite as cookies no seu navegador!", "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 ten permiso para acceder a este documento", "pad.permissionDenied": "Non tes permiso para acceder a este documento",
"pad.settings.padSettings": "Configuracións do documento", "pad.settings.padSettings": "Configuracións do documento",
"pad.settings.myView": "A miña vista", "pad.settings.myView": "A miña vista",
"pad.settings.stickychat": "Chat sempre visible", "pad.settings.stickychat": "Chat sempre visible",
"pad.settings.chatandusers": "Mostrar o chat e os usuarios", "pad.settings.chatandusers": "Mostrar o chat e os usuarios",
"pad.settings.colorcheck": "Cores de identificación", "pad.settings.colorcheck": "Cores de identificación",
"pad.settings.linenocheck": "Números de liña", "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": "Tipo de letra:",
"pad.settings.fontType.normal": "Normal", "pad.settings.fontType.normal": "Normal",
"pad.settings.language": "Lingua:", "pad.settings.language": "Lingua:",
"pad.settings.about": "Acerca de",
"pad.settings.poweredBy": "Grazas a",
"pad.importExport.import_export": "Importar/Exportar", "pad.importExport.import_export": "Importar/Exportar",
"pad.importExport.import": "Cargar un ficheiro de texto ou documento", "pad.importExport.import": "Cargar un ficheiro de texto ou documento",
"pad.importExport.importSuccessful": "Correcto!", "pad.importExport.importSuccessful": "Correcto!",
@ -49,9 +86,9 @@
"pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF", "pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)", "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 <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instale AbiWord</a>.", "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 <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instala AbiWord</a>.",
"pad.modals.connected": "Conectado.", "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.forcereconnect": "Forzar a reconexión",
"pad.modals.reconnecttimer": "Intentarase reconectar en", "pad.modals.reconnecttimer": "Intentarase reconectar en",
"pad.modals.cancel": "Cancelar", "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.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": "Borrado.",
"pad.modals.deleted.explanation": "Este documento foi eliminado.", "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": "Foi desconectado.",
"pad.modals.disconnected.explanation": "Perdeuse a conexión co servidor", "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.", "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": "Chat",
"pad.chat.title": "Abrir o chat deste documento.", "pad.chat.title": "Abrir o chat deste documento.",
"pad.chat.loadmessages": "Cargar máis mensaxes", "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.pageTitle": "Liña do tempo de {{appTitle}}",
"timeslider.toolbar.returnbutton": "Volver ao documento", "timeslider.toolbar.returnbutton": "Volver ao documento",
"timeslider.toolbar.authors": "Autores:", "timeslider.toolbar.authors": "Autores:",
@ -112,7 +156,7 @@
"pad.savedrevs.timeslider": "Pode consultar as revisións gardadas visitando a liña do tempo", "pad.savedrevs.timeslider": "Pode consultar as revisións gardadas visitando a liña do tempo",
"pad.userlist.entername": "Insira o seu nome", "pad.userlist.entername": "Insira o seu nome",
"pad.userlist.unnamed": "anónimo", "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.importbutton": "Importar agora",
"pad.impexp.importing": "Importando...", "pad.impexp.importing": "Importando...",
"pad.impexp.confirmimport": "A importación dun ficheiro ha sobrescribir o texto actual do documento. Está seguro de querer continuar?", "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.uploadFailed": "Houbo un erro ao cargar o ficheiro; inténteo de novo",
"pad.impexp.importfailed": "Fallou a importación", "pad.impexp.importfailed": "Fallou a importación",
"pad.impexp.copypaste": "Copie e pegue", "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"
} }

View file

@ -4,6 +4,7 @@
"Athena in Wonderland", "Athena in Wonderland",
"Cainamarques", "Cainamarques",
"GoEThe", "GoEThe",
"Guilha",
"Hamilton Abreu", "Hamilton Abreu",
"Imperadeiro98", "Imperadeiro98",
"Luckas", "Luckas",
@ -16,9 +17,42 @@
"Waldyrious" "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.newPad": "Nova Nota",
"index.createOpenPad": "ou crie/abra uma nota com o nome:", "index.createOpenPad": "ou cria/abre uma nota com o nome:",
"index.openPad": "abrir uma «Nota» existente com o nome:", "index.openPad": "abrir uma Nota existente com o nome:",
"pad.toolbar.bold.title": "Negrito (Ctrl+B)", "pad.toolbar.bold.title": "Negrito (Ctrl+B)",
"pad.toolbar.italic.title": "Itálico (Ctrl+I)", "pad.toolbar.italic.title": "Itálico (Ctrl+I)",
"pad.toolbar.underline.title": "Sublinhado (Ctrl+U)", "pad.toolbar.underline.title": "Sublinhado (Ctrl+U)",
@ -26,7 +60,7 @@
"pad.toolbar.ol.title": "Lista ordenada (Ctrl+Shift+N)", "pad.toolbar.ol.title": "Lista ordenada (Ctrl+Shift+N)",
"pad.toolbar.ul.title": "Lista desordenada (Ctrl+Shift+L)", "pad.toolbar.ul.title": "Lista desordenada (Ctrl+Shift+L)",
"pad.toolbar.indent.title": "Indentar (TAB)", "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.undo.title": "Desfazer (Ctrl+Z)",
"pad.toolbar.redo.title": "Refazer (Ctrl+Y)", "pad.toolbar.redo.title": "Refazer (Ctrl+Y)",
"pad.toolbar.clearAuthorship.title": "Limpar cores de autoria (Ctrl+Shift+C)", "pad.toolbar.clearAuthorship.title": "Limpar cores de autoria (Ctrl+Shift+C)",
@ -65,7 +99,7 @@
"pad.importExport.exportopen": "ODF (Open Document Format)", "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, <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instale AbiWord ou LibreOffice</a>, por favor.", "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, <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instale AbiWord ou LibreOffice</a>, por favor.",
"pad.modals.connected": "Ligado.", "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.forcereconnect": "Forçar restabelecimento de ligação",
"pad.modals.reconnecttimer": "A tentar restabelecer ligação", "pad.modals.reconnecttimer": "A tentar restabelecer ligação",
"pad.modals.cancel": "Cancelar", "pad.modals.cancel": "Cancelar",
@ -89,6 +123,8 @@
"pad.modals.deleted.explanation": "Esta nota foi removida.", "pad.modals.deleted.explanation": "Esta nota foi removida.",
"pad.modals.rateLimited": "Limitado.", "pad.modals.rateLimited": "Limitado.",
"pad.modals.rateLimited.explanation": "Enviou demasiadas mensagens para este pad, por isso foi desligado.", "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": "Você foi desligado.",
"pad.modals.disconnected.explanation": "A ligação ao servidor foi perdida", "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.", "pad.modals.disconnected.cause": "O servidor pode estar indisponível. Por favor, notifique o administrador de serviço se isto continuar a acontecer.",

View file

@ -1,7 +1,7 @@
'use strict'; '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 * provided by the settings module
*/ */
@ -36,7 +36,7 @@ const db =
exports.db = null; 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 * @param {Function} callback
*/ */
exports.init = async () => await new Promise((resolve, reject) => { exports.init = async () => await new Promise((resolve, reject) => {

View file

@ -139,7 +139,7 @@ exports.getPad = async (id, text) => {
// try to load pad // try to load pad
pad = new Pad(id); pad = new Pad(id);
// initalize the pad // initialize the pad
await pad.init(text); await pad.init(text);
globalPads.set(id, pad); globalPads.set(id, pad);
padList.addPad(id); padList.addPad(id);

View file

@ -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 * key = sessionId
* values = padId, readonlyPadId, readonly, author, rev * values = padId, readonlyPadId, readonly, author, rev
* padId = the real padId of the pad * padId = the real padId of the pad
@ -88,7 +88,7 @@ exports.setSocketIO = (socket_io) => {
exports.handleConnect = (socket) => { exports.handleConnect = (socket) => {
stats.meter('connects').mark(); stats.meter('connects').mark();
// Initalize sessioninfos for this new session // Initialize sessioninfos for this new session
sessioninfos[socket.id] = {}; sessioninfos[socket.id] = {};
}; };

View file

@ -141,7 +141,7 @@ const resources = {
// We need an operation that returns a SessionInfo so it can be picked up by the codegen :( // We need an operation that returns a SessionInfo so it can be picked up by the codegen :(
info: { info: {
operationId: 'getSessionInfo', operationId: 'getSessionInfo',
summary: 'returns informations about a session', summary: 'returns information about a session',
responseSchema: {info: {$ref: '#/components/schemas/SessionInfo'}}, responseSchema: {info: {$ref: '#/components/schemas/SessionInfo'}},
}, },
}, },

View file

@ -88,7 +88,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
// http://stackoverflow.com/questions/23981741/minify-socket-io-socket-io-js-with-1-0 // http://stackoverflow.com/questions/23981741/minify-socket-io-socket-io-js-with-1-0
// if(settings.minify) io.enable('browser client minification'); // if(settings.minify) io.enable('browser client minification');
// Initalize the Socket.IO Router // Initialize the Socket.IO Router
socketIORouter.setSocketIO(io); socketIORouter.setSocketIO(io);
socketIORouter.addComponent('pad', padMessageHandler); socketIORouter.addComponent('pad', padMessageHandler);

View file

@ -36,8 +36,8 @@ const wtfnode = require('wtfnode');
* any modules that require newer versions of NodeJS * any modules that require newer versions of NodeJS
*/ */
const NodeVersion = require('./utils/NodeVersion'); const NodeVersion = require('./utils/NodeVersion');
NodeVersion.enforceMinNodeVersion('10.13.0'); NodeVersion.enforceMinNodeVersion('10.17.0');
NodeVersion.checkDeprecationStatus('10.13.0', '1.8.3'); NodeVersion.checkDeprecationStatus('10.17.0', '1.8.8');
const UpdateCheck = require('./utils/UpdateCheck'); const UpdateCheck = require('./utils/UpdateCheck');
const db = require('./db/DB'); const db = require('./db/DB');

View file

@ -311,7 +311,7 @@ const statFile = (filename, callback, dirStatLimit) => {
const lastModifiedDateOfEverything = (callback) => { const lastModifiedDateOfEverything = (callback) => {
const folders2check = [`${ROOT_DIR}js/`, `${ROOT_DIR}css/`]; const folders2check = [`${ROOT_DIR}js/`, `${ROOT_DIR}css/`];
let latestModification = 0; let latestModification = 0;
// go trough this two folders // go through this two folders
async.forEach(folders2check, (path, callback) => { async.forEach(folders2check, (path, callback) => {
// read the files in the folder // read the files in the folder
fs.readdir(path, (err, files) => { fs.readdir(path, (err, files) => {
@ -320,7 +320,7 @@ const lastModifiedDateOfEverything = (callback) => {
// we wanna check the directory itself for changes too // we wanna check the directory itself for changes too
files.push('.'); files.push('.');
// go trough all files in this folder // go through all files in this folder
async.forEach(files, (filename, callback) => { async.forEach(files, (filename, callback) => {
// get the stat data of this file // get the stat data of this file
fs.stat(`${path}/${filename}`, (err, stats) => { fs.stat(`${path}/${filename}`, (err, stats) => {

View file

@ -139,7 +139,7 @@
"root": true "root": true
}, },
"engines": { "engines": {
"node": ">=10.13.0", "node": "^10.17.0 || >=11.14.0",
"npm": ">=5.5.1" "npm": ">=5.5.1"
}, },
"repository": { "repository": {

View file

@ -93,11 +93,8 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool
} else if (linestylefilter.ATTRIB_CLASSES[key]) { } else if (linestylefilter.ATTRIB_CLASSES[key]) {
classes += ` ${linestylefilter.ATTRIB_CLASSES[key]}`; classes += ` ${linestylefilter.ATTRIB_CLASSES[key]}`;
} else { } else {
classes += hooks.callAllStr('aceAttribsToClasses', { const results = hooks.callAll('aceAttribsToClasses', {linestylefilter, key, value});
linestylefilter, classes += ` ${results.join(' ')}`;
key,
value,
}, ' ', ' ', '');
} }
} }
} }

View file

@ -294,7 +294,7 @@ const handshake = () => {
// set some client vars // set some client vars
window.clientVars = obj.data; window.clientVars = obj.data;
// initalize the pad // initialize the pad
pad._afterHandshake(); pad._afterHandshake();
if (clientVars.readonly) { if (clientVars.readonly) {

View file

@ -1,6 +1,5 @@
/* global exports, require */ 'use strict';
const _ = require('underscore');
const pluginDefs = require('./plugin_defs'); const pluginDefs = require('./plugin_defs');
// Maps the name of a server-side hook to a string explaining the deprecation // Maps the name of a server-side hook to a string explaining the deprecation
@ -15,66 +14,37 @@ exports.deprecationNotices = {};
const deprecationWarned = {}; const deprecationWarned = {};
function checkDeprecation(hook) { const checkDeprecation = (hook) => {
const notice = exports.deprecationNotices[hook.hook_name]; const notice = exports.deprecationNotices[hook.hook_name];
if (notice == null) return; if (notice == null) return;
if (deprecationWarned[hook.hook_fn_name]) return; if (deprecationWarned[hook.hook_fn_name]) return;
console.warn(`${hook.hook_name} hook used by the ${hook.part.plugin} plugin ` + console.warn(`${hook.hook_name} hook used by the ${hook.part.plugin} plugin ` +
`(${hook.hook_fn_name}) is deprecated: ${notice}`); `(${hook.hook_fn_name}) is deprecated: ${notice}`);
deprecationWarned[hook.hook_fn_name] = true; 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) { // Calls the node-style callback when the Promise settles. Unlike util.callbackify, this takes a
return normalizedhook(); // Promise (rather than a function that returns a Promise), and it returns a Promise (rather than a
} else { // function that returns undefined).
try { const attachCallback = (p, cb) => p.then(
return normalizedhook(); (val) => cb(null, val),
} catch (ex) { // Callbacks often only check the truthiness, not the nullness, of the first parameter. To avoid
console.error([hook_name, hook.part.full_name, ex.stack || ex]); // 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.syncMapFirst = function (lst, fn) { // Flattens the array one level.
let i; const flatten1 = (array) => array.reduce((a, b) => a.concat(b), []);
let result;
for (i = 0; i < lst.length; i++) {
result = fn(lst[i]);
if (result.length) return result;
}
return [];
};
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();
};
// Calls the hook function synchronously and returns the value provided by the hook function (via // Calls the hook function synchronously and returns the value provided by the hook function (via
// callback or return value). // 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. // 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); checkDeprecation(hook);
// This var is used to keep track of whether the hook function already settled. // 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 // The hook function is assumed to not have a callback parameter, so fall through and accept
// `undefined` as the resolved value. // `undefined` as the resolved value.
// //
// IMPORTANT: "Rest" parameters and default parameters are not counted in`Function.length`, so // IMPORTANT: "Rest" parameters and default parameters are not included in `Function.length`,
// the assumption does not hold for wrappers like `(...args) => { real(...args); }`. Such // so the assumption does not hold for wrappers such as:
// functions will still work properly without any logged warnings or errors for now, but: //
// 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 // * 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. // resolve to `undefined` instead of the desired value.
//
// * The above "unsettled function" warning is not logged if the function fails to call the // * The above "unsettled function" warning is not logged if the function fails to call the
// callback like it is supposed to. // 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'); settle(null, val, 'returned value');
return outcome.val; return outcome.val;
} };
// DEPRECATED: Use `callAllSerial()` or `aCallAll()` instead.
//
// Invokes all registered hook functions synchronously. // Invokes all registered hook functions synchronously.
// //
// Arguments: // Arguments:
@ -203,15 +188,10 @@ function callHookFnSync(hook, context) {
// 1. Collect all values returned by the hook functions into an array. // 1. Collect all values returned by the hook functions into an array.
// 2. Convert each `undefined` entry into `[]`. // 2. Convert each `undefined` entry into `[]`.
// 3. Flatten one level. // 3. Flatten one level.
exports.callAll = function (hookName, context) { exports.callAll = (hookName, context) => {
if (context == null) context = {}; if (context == null) context = {};
const hooks = pluginDefs.hooks[hookName] || []; const hooks = pluginDefs.hooks[hookName] || [];
return _.flatten(hooks.map((hook) => { return flatten1(hooks.map((hook) => normalizeValue(callHookFnSync(hook, context))));
const ret = callHookFnSync(hook, context);
// `undefined` (but not `null`!) is treated the same as [].
if (ret === undefined) return [];
return ret;
}), 1);
}; };
// Calls the hook function asynchronously and returns a Promise that either resolves to the hook // 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. // 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); checkDeprecation(hook);
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
// This var is used to keep track of whether the hook function already settled. // 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 // The hook function is assumed to not have a callback parameter, so fall through and accept
// `undefined` as the resolved value. // `undefined` as the resolved value.
// //
// IMPORTANT: "Rest" parameters and default parameters are not counted in `Function.length`, // IMPORTANT: "Rest" parameters and default parameters are not included in
// so the assumption does not hold for wrappers like `(...args) => { real(...args); }`. For // `Function.length`, so the assumption does not hold for wrappers such as:
// 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. // 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'), (val) => settle(null, val, 'returned value'),
(err) => settle(err, null, 'Promise rejection')); (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: // Arguments:
// * hookName: Name of the hook to invoke. // * hookName: Name of the hook to invoke.
// * context: Passed unmodified to the hook functions, except nullish becomes {}. // * 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); // const p1 = hooks.aCallAll('myHook', context, cb);
// is equivalent to: // 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: // Return value:
// If cb is nullish, this function resolves to a flattened array of hook results. Specifically, it // 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 `[]`. // 2. Convert each `undefined` entry into `[]`.
// 3. Flatten one level. // 3. Flatten one level.
// If cb is non-null, this function resolves to the value returned by cb. // 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 = {}; if (context == null) context = {};
const hooks = pluginDefs.hooks[hookName] || []; const hooks = pluginDefs.hooks[hookName] || [];
let resultsPromise = Promise.all(hooks.map((hook) => callHookFnAsync(hook, context) const results = await Promise.all(
// `undefined` (but not `null`!) is treated the same as []. hooks.map(async (hook) => normalizeValue(await callHookFnAsync(hook, context))));
.then((result) => (result === undefined) ? [] : result))).then((results) => _.flatten(results, 1)); return flatten1(results);
if (cb != null) resultsPromise = resultsPromise.then((val) => cb(null, val), cb);
return await resultsPromise;
}; };
exports.callFirst = function (hook_name, args) { // Like `aCallAll()` except the hook functions are called one at a time instead of concurrently.
if (!args) args = {}; // Only use this function if the hook functions must be called one at a time, otherwise use
if (pluginDefs.hooks[hook_name] === undefined) return []; // `aCallAll()`.
return exports.syncMapFirst(pluginDefs.hooks[hook_name], (hook) => hookCallWrapper(hook, hook_name, args)); 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);
}; };
function aCallFirst(hook_name, args, cb, predicate) { // DEPRECATED: Use `aCallFirst()` instead.
if (!args) args = {}; //
if (!cb) cb = function () {}; // Like `aCallFirst()`, but synchronous. Hook functions must provide their values synchronously.
if (pluginDefs.hooks[hook_name] === undefined) return cb(null, []); exports.callFirst = (hookName, context) => {
exports.mapFirst( if (context == null) context = {};
pluginDefs.hooks[hook_name], const predicate = (val) => val.length;
(hook, cb) => { const hooks = pluginDefs.hooks[hookName] || [];
hookCallWrapper(hook, hook_name, args, (res) => { cb(null, res); }); for (const hook of hooks) {
}, const val = normalizeValue(callHookFnSync(hook, context));
cb, if (predicate(val)) return val;
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);
} }
return [];
}; };
exports.callAllStr = function (hook_name, args, sep, pre, post) { // Invokes the registered hook functions one at a time until one provides a value that meets a
if (sep == undefined) sep = ''; // customizable condition.
if (pre == undefined) pre = ''; //
if (post == undefined) post = ''; // Arguments:
const newCallhooks = []; // * hookName: Name of the hook to invoke.
const callhooks = exports.callAll(hook_name, args); // * context: Passed unmodified to the hook functions, except nullish becomes {}.
for (let i = 0, ii = callhooks.length; i < ii; i++) { // * cb: Deprecated callback. The following:
newCallhooks[i] = pre + callhooks[i] + post; // 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);
} }
return newCallhooks.join(sep || ''); 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 = { exports.exportedForTestingOnly = {

View file

@ -1,6 +1,8 @@
'use strict';
const log4js = require('log4js'); const log4js = require('log4js');
const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); const plugins = require('./plugins');
const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); const hooks = require('./hooks');
const npm = require('npm'); const npm = require('npm');
const request = require('request'); const request = require('request');
const util = require('util'); const util = require('util');
@ -13,22 +15,22 @@ const loadNpm = async () => {
npm.on('log', log4js.getLogger('npm').log); npm.on('log', log4js.getLogger('npm').log);
}; };
const onAllTasksFinished = () => {
hooks.aCallAll('restartServer', {}, () => {});
};
let tasks = 0; let tasks = 0;
function wrapTaskCb(cb) { function wrapTaskCb(cb) {
tasks++; tasks++;
return function () { return function (...args) {
cb && cb.apply(this, arguments); cb && cb.apply(this, args);
tasks--; tasks--;
if (tasks == 0) onAllTasksFinished(); if (tasks === 0) onAllTasksFinished();
}; };
} }
function onAllTasksFinished() {
hooks.aCallAll('restartServer', {}, () => {});
}
exports.uninstall = async (pluginName, cb = null) => { exports.uninstall = async (pluginName, cb = null) => {
cb = wrapTaskCb(cb); cb = wrapTaskCb(cb);
try { try {
@ -60,7 +62,7 @@ exports.install = async (pluginName, cb = null) => {
exports.availablePlugins = null; exports.availablePlugins = null;
let cacheTimestamp = 0; let cacheTimestamp = 0;
exports.getAvailablePlugins = function (maxCacheAge) { exports.getAvailablePlugins = (maxCacheAge) => {
const nowTimestamp = Math.round(Date.now() / 1000); const nowTimestamp = Math.round(Date.now() / 1000);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -87,8 +89,8 @@ exports.getAvailablePlugins = function (maxCacheAge) {
}; };
exports.search = function (searchTerm, maxCacheAge) { exports.search = (searchTerm, maxCacheAge) => exports.getAvailablePlugins(maxCacheAge).then(
return exports.getAvailablePlugins(maxCacheAge).then((results) => { (results) => {
const res = {}; const res = {};
if (searchTerm) { if (searchTerm) {
@ -97,10 +99,12 @@ exports.search = function (searchTerm, maxCacheAge) {
for (const pluginName in results) { for (const pluginName in results) {
// for every available plugin // for every available plugin
if (pluginName.indexOf(plugins.prefix) != 0) continue; // TODO: Also search in keywords here! // TODO: Also search in keywords here!
if (pluginName.indexOf(plugins.prefix) !== 0) continue;
if (searchTerm && !~results[pluginName].name.toLowerCase().indexOf(searchTerm) && if (searchTerm && !~results[pluginName].name.toLowerCase().indexOf(searchTerm) &&
(typeof results[pluginName].description !== 'undefined' && !~results[pluginName].description.toLowerCase().indexOf(searchTerm)) (typeof results[pluginName].description !== 'undefined' &&
!~results[pluginName].description.toLowerCase().indexOf(searchTerm))
) { ) {
if (typeof results[pluginName].description === 'undefined') { if (typeof results[pluginName].description === 'undefined') {
console.debug('plugin without Description: %s', results[pluginName].name); console.debug('plugin without Description: %s', results[pluginName].name);
@ -113,5 +117,5 @@ exports.search = function (searchTerm, maxCacheAge) {
} }
return res; return res;
}); }
}; );

View file

@ -1,3 +1,5 @@
'use strict';
// This module contains processed plugin definitions. The data structures in this file are set by // This module contains processed plugin definitions. The data structures in this file are set by
// plugins.js (server) or client_plugins.js (client). // plugins.js (server) or client_plugins.js (client).

View file

@ -1,6 +1,7 @@
'use strict';
const fs = require('fs').promises; const fs = require('fs').promises;
const hooks = require('./hooks'); const hooks = require('./hooks');
const npm = require('npm/lib/npm.js');
const readInstalled = require('./read-installed.js'); const readInstalled = require('./read-installed.js');
const path = require('path'); const path = require('path');
const tsort = require('./tsort'); const tsort = require('./tsort');
@ -13,11 +14,9 @@ const defs = require('./plugin_defs');
exports.prefix = 'ep_'; exports.prefix = 'ep_';
exports.formatPlugins = function () { exports.formatPlugins = () => Object.keys(defs.plugins).join(', ');
return _.keys(defs.plugins).join(', ');
};
exports.formatPluginsWithVersion = function () { exports.formatPluginsWithVersion = () => {
const plugins = []; const plugins = [];
_.forEach(defs.plugins, (plugin) => { _.forEach(defs.plugins, (plugin) => {
if (plugin.package.name !== 'ep_etherpad-lite') { if (plugin.package.name !== 'ep_etherpad-lite') {
@ -28,17 +27,16 @@ exports.formatPluginsWithVersion = function () {
return plugins.join(', '); return plugins.join(', ');
}; };
exports.formatParts = function () { exports.formatParts = () => _.map(defs.parts, (part) => part.full_name).join('\n');
return _.map(defs.parts, (part) => part.full_name).join('\n');
};
exports.formatHooks = function (hook_set_name) { exports.formatHooks = (hook_set_name) => {
const res = []; const res = [];
const hooks = pluginUtils.extractHooks(defs.parts, hook_set_name || 'hooks'); const hooks = pluginUtils.extractHooks(defs.parts, hook_set_name || 'hooks');
_.chain(hooks).keys().forEach((hook_name) => { _.chain(hooks).keys().forEach((hook_name) => {
_.forEach(hooks[hook_name], (hook) => { _.forEach(hooks[hook_name], (hook) => {
res.push(`<dt>${hook.hook_name}</dt><dd>${hook.hook_fn_name} from ${hook.part.full_name}</dd>`); res.push(`<dt>${hook.hook_name}</dt><dd>${hook.hook_fn_name} ` +
`from ${hook.part.full_name}</dd>`);
}); });
}); });
return `<dl>${res.join('\n')}</dl>`; return `<dl>${res.join('\n')}</dl>`;
@ -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'. 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'. // If there is a single colon assume it's 'filename:funcname' not 'C:\\filename'.
const functionName = (tmp.length > 1 ? tmp.pop() : null) || hook_name; 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}`; return `${fileName}:${functionName}`;
}; };
exports.update = async function () { exports.update = async () => {
const packages = await exports.getPackages(); const packages = await exports.getPackages();
const parts = {}; // Key is full name. sortParts converts this into a topologically sorted array. const parts = {}; // Key is full name. sortParts converts this into a topologically sorted array.
const plugins = {}; const plugins = {};
@ -83,13 +81,14 @@ exports.update = async function () {
await callInit(); await callInit();
}; };
exports.getPackages = async function () { exports.getPackages = async () => {
// Load list of installed NPM packages, flatten it to a list, and filter out only packages with names that // Load list of installed NPM packages, flatten it to a list,
// and filter out only packages with names that
const dir = settings.root; const dir = settings.root;
const data = await util.promisify(readInstalled)(dir); const data = await util.promisify(readInstalled)(dir);
const packages = {}; const packages = {};
function flatten(deps) { const flatten = (deps) => {
_.chain(deps).keys().each((name) => { _.chain(deps).keys().each((name) => {
if (name.indexOf(exports.prefix) === 0) { if (name.indexOf(exports.prefix) === 0) {
packages[name] = _.clone(deps[name]); packages[name] = _.clone(deps[name]);
@ -102,7 +101,7 @@ exports.getPackages = async function () {
// I don't think we need recursion // I don't think we need recursion
// if (deps[name].dependencies !== undefined) flatten(deps[name].dependencies); // if (deps[name].dependencies !== undefined) flatten(deps[name].dependencies);
}); });
} };
const tmp = {}; const tmp = {};
tmp[data.name] = data; tmp[data.name] = data;
@ -110,7 +109,7 @@ exports.getPackages = async function () {
return packages; 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'); const plugin_path = path.resolve(packages[plugin_name].path, 'ep.json');
try { try {
const data = await fs.readFile(plugin_path); const data = await fs.readFile(plugin_path);
@ -129,9 +128,9 @@ async function loadPlugin(packages, plugin_name, plugins, parts) {
} catch (er) { } catch (er) {
console.error(`Unable to load plugin definition file ${plugin_path}`); console.error(`Unable to load plugin definition file ${plugin_path}`);
} }
} };
function partsToParentChildList(parts) { const partsToParentChildList = (parts) => {
const res = []; const res = [];
_.chain(parts).keys().forEach((name) => { _.chain(parts).keys().forEach((name) => {
_.each(parts[name].post || [], (child_name) => { _.each(parts[name].post || [], (child_name) => {
@ -145,15 +144,9 @@ function partsToParentChildList(parts) {
} }
}); });
return res; return res;
} };
// Used only in Node, so no need for _ // Used only in Node, so no need for _
function sortParts(parts) { const sortParts = (parts) => tsort(partsToParentChildList(parts))
return tsort( .filter((name) => parts[name] !== undefined)
partsToParentChildList(parts) .map((name) => parts[name]);
).filter(
(name) => parts[name] !== undefined
).map(
(name) => parts[name]
);
}

View file

@ -1,3 +1,4 @@
'use strict';
const _ = require('underscore'); const _ = require('underscore');
const defs = require('./plugin_defs'); const defs = require('./plugin_defs');
@ -8,13 +9,13 @@ const disabledHookReasons = {
}, },
}; };
function loadFn(path, hookName) { const loadFn = (path, hookName) => {
let functionName; let functionName;
const parts = path.split(':'); const parts = path.split(':');
// on windows: C:\foo\bar:xyz // on windows: C:\foo\bar:xyz
if (parts[0].length == 1) { if (parts[0].length === 1) {
if (parts.length == 3) { if (parts.length === 3) {
functionName = parts.pop(); functionName = parts.pop();
} }
path = parts.join(':'); path = parts.join(':');
@ -30,9 +31,9 @@ function loadFn(path, hookName) {
fn = fn[name]; fn = fn[name];
}); });
return fn; return fn;
} };
function extractHooks(parts, hook_set_name, normalizer) { const extractHooks = (parts, hook_set_name, normalizer) => {
const hooks = {}; const hooks = {};
_.each(parts, (part) => { _.each(parts, (part) => {
_.chain(part[hook_set_name] || {}) _.chain(part[hook_set_name] || {})
@ -50,20 +51,23 @@ function extractHooks(parts, hook_set_name, normalizer) {
const disabledReason = (disabledHookReasons[hook_set_name] || {})[hook_name]; const disabledReason = (disabledHookReasons[hook_set_name] || {})[hook_name];
if (disabledReason) { 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} ` + console.error(`The hook function ${hook_fn_name} from plugin ${part.plugin} ` +
'will never be called, which may cause the plugin to fail'); '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`); console.error(
`Please update the ${part.plugin} plugin to not use the ${hook_name} hook`);
return; return;
} }
let hook_fn;
try { try {
var hook_fn = loadFn(hook_fn_name, hook_name); hook_fn = loadFn(hook_fn_name, hook_name);
if (!hook_fn) { if (!hook_fn) {
throw 'Not a function'; throw new Error('Not a function');
} }
} catch (exc) { } 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 (hook_fn) {
if (hooks[hook_name] == null) hooks[hook_name] = []; if (hooks[hook_name] == null) hooks[hook_name] = [];
@ -72,7 +76,7 @@ function extractHooks(parts, hook_set_name, normalizer) {
}); });
}); });
return hooks; return hooks;
} };
exports.extractHooks = extractHooks; exports.extractHooks = extractHooks;
@ -88,10 +92,10 @@ exports.extractHooks = extractHooks;
* No plugins: [] * No plugins: []
* Some plugins: [ 'ep_adminpads', 'ep_add_buttons', 'ep_activepads' ] * Some plugins: [ 'ep_adminpads', 'ep_add_buttons', 'ep_activepads' ]
*/ */
exports.clientPluginNames = function () { exports.clientPluginNames = () => {
const client_plugin_names = _.uniq( const client_plugin_names = _.uniq(
defs.parts defs.parts
.filter((part) => part.hasOwnProperty('client_hooks')) .filter((part) => Object.prototype.hasOwnProperty.call(part, 'client_hooks'))
.map((part) => `plugin-${part.plugin}`) .map((part) => `plugin-${part.plugin}`)
); );

View file

@ -42,7 +42,7 @@ function runTest(number) {
let fN = '/test.txt'; let fN = '/test.txt';
let cT = 'text/plain'; 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 // We provide a weird file name and also set a weird contentType
if (number % 2 == 0) { if (number % 2 == 0) {
fN = froth().toString(); fN = froth().toString();

View file

@ -94,7 +94,7 @@ const testImports = {
wantText: ' word1 word2 word3\n\n', wantText: ' word1 word2 word3\n\n',
}, },
'nonBreakingSpacePreceededBySpaceBetweenWords': { 'nonBreakingSpacePreceededBySpaceBetweenWords': {
description: 'A non-breaking space preceeded by a normal space', description: 'A non-breaking space preceded by a normal space',
input: '<html><body> &nbsp;word1 &nbsp;word2 &nbsp;word3<br></body></html>', input: '<html><body> &nbsp;word1 &nbsp;word2 &nbsp;word3<br></body></html>',
wantHTML: '<!DOCTYPE HTML><html><body>&nbsp;word1&nbsp; word2&nbsp; word3<br><br></body></html>', wantHTML: '<!DOCTYPE HTML><html><body>&nbsp;word1&nbsp; word2&nbsp; word3<br><br></body></html>',
wantText: ' word1 word2 word3\n\n', wantText: ' word1 word2 word3\n\n',
@ -191,7 +191,7 @@ const testImports = {
wantText: 'а б в г ґ д е є ж з и і ї й к л м н о\nmultiple\n lines\n in\n pre\n\nп р с т у ф х ц ч ш щ ю я ь\n\n', wantText: 'а б в г ґ д е є ж з и і ї й к л м н о\nmultiple\n lines\n in\n pre\n\nп р с т у ф х ц ч ш щ ю я ь\n\n',
}, },
'preIntroducesASpace': { '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: `<html><body><p> input: `<html><body><p>
1 1
<pre>preline <pre>preline

View file

@ -107,9 +107,9 @@ describe(__filename, function () {
-> setText(padId, "hello world") -> setText(padId, "hello world")
-> getLastEdited(padID) -- Should be when pad was made -> getLastEdited(padID) -- Should be when pad was made
-> getText(padId) -- Should be "hello world" -> 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" -> 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" -> getText(originalPadId) -- Should be "hello world"
-> getLastEdited(padID) -- Should not be 0 -> getLastEdited(padID) -- Should not be 0
-> appendText(padID, "hello") -> appendText(padID, "hello")

View file

@ -100,6 +100,83 @@ describe(__filename, function () {
assert(res.body.data.groupID); 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 () { describe('API: Author creation', function () {

View file

@ -140,7 +140,7 @@ const tests = {
wantText: [' word1 word2 word3'], wantText: [' word1 word2 word3'],
}, },
nonBreakingSpacePreceededBySpaceBetweenWords: { nonBreakingSpacePreceededBySpaceBetweenWords: {
description: 'A non-breaking space preceeded by a normal space', description: 'A non-breaking space preceded by a normal space',
html: '<html><body> &nbsp;word1 &nbsp;word2 &nbsp;word3<br></body></html>', html: '<html><body> &nbsp;word1 &nbsp;word2 &nbsp;word3<br></body></html>',
wantLineAttribs: ['+l'], wantLineAttribs: ['+l'],
wantText: [' word1 word2 word3'], wantText: [' word1 word2 word3'],
@ -240,7 +240,7 @@ pre
], ],
}, },
preIntroducesASpace: { 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: `<html><body><p> html: `<html><body><p>
1 1
</p><pre>preline </p><pre>preline

View file

@ -1,11 +1,8 @@
'use strict'; 'use strict';
function m(mod) { return `${__dirname}/../../../src/${mod}`; }
const assert = require('assert').strict; const assert = require('assert').strict;
const hooks = require(m('static/js/pluginfw/hooks')); const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks');
const plugins = require(m('static/js/pluginfw/plugin_defs')); const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs');
const sinon = require(m('node_modules/sinon')); const sinon = require('ep_etherpad-lite/node_modules/sinon');
describe(__filename, function () { describe(__filename, function () {
const hookName = 'testHook'; 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 // 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.) // time, or call the callback and then return a value.)
describe('bad hook function behavior (double settle)', function () { describe('bad hook function behavior (double settle)', function () {
beforeEach(function () { beforeEach(async function () {
sinon.stub(console, 'error'); 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 () { describe('callHookFnAsync', function () {
const callHookFnAsync = hooks.exportedForTestingOnly.callHookFnAsync; // Convenience shorthand. 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 // 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.) // time, or call the callback and then return a value.)
describe('bad hook function behavior (double settle)', function () { describe('bad hook function behavior (double settle)', function () {
beforeEach(function () { beforeEach(async function () {
sinon.stub(console, 'error'); 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!
});
});
}); });

View file

@ -1,11 +1,9 @@
'use strict'; 'use strict';
function m(mod) { return `${__dirname}/../../../src/${mod}`; }
const assert = require('assert').strict; const assert = require('assert').strict;
const common = require('../common'); const common = require('../common');
const plugins = require(m('static/js/pluginfw/plugin_defs')); const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs');
const settings = require(m('node/utils/Settings')); const settings = require('ep_etherpad-lite/node/utils/Settings');
describe(__filename, function () { describe(__filename, function () {
this.timeout(30000); this.timeout(30000);
@ -13,6 +11,13 @@ describe(__filename, function () {
const backups = {}; const backups = {};
const authHookNames = ['preAuthorize', 'authenticate', 'authorize']; const authHookNames = ['preAuthorize', 'authenticate', 'authorize'];
const failHookNames = ['preAuthzFailure', 'authnFailure', 'authzFailure', 'authFailure']; 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(); }); before(async function () { agent = await common.init(); });
beforeEach(async function () { beforeEach(async function () {
backups.hooks = {}; backups.hooks = {};
@ -154,7 +159,10 @@ describe(__filename, function () {
const h0 = new Handler(hookName, '_0'); const h0 = new Handler(hookName, '_0');
const h1 = new Handler(hookName, '_1'); const h1 = new Handler(hookName, '_1');
handlers[hookName] = [h0, h1]; 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); this.timeout(100);
handlers.preAuthorize[0].innerHandle = () => [false]; handlers.preAuthorize[0].innerHandle = () => [false];
let called = 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.equal(hookName, 'preAuthzFailure');
assert(req != null); assert(req != null);
assert(res != null); assert(res != null);
@ -225,7 +233,7 @@ describe(__filename, function () {
called = true; called = true;
res.status(200).send('injected'); res.status(200).send('injected');
return cb([true]); return cb([true]);
}}]; })];
await agent.get('/admin/').auth('admin', 'admin-password').expect(200, 'injected'); await agent.get('/admin/').auth('admin', 'admin-password').expect(200, 'injected');
assert(called); assert(called);
}); });
@ -441,11 +449,11 @@ describe(__filename, function () {
}; };
const handlers = {}; const handlers = {};
beforeEach(function () { beforeEach(async function () {
failHookNames.forEach((hookName) => { failHookNames.forEach((hookName) => {
const handler = new Handler(hookName); const handler = new Handler(hookName);
handlers[hookName] = handler; 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.requireAuthentication = true;
settings.requireAuthorization = true; settings.requireAuthorization = true;

View file

@ -1,5 +1,5 @@
'use strict'; 'use strict';
const helper = {}; // eslint-disable-line const helper = {}; // eslint-disable-line no-redeclare
(function () { (function () {
let $iframe; const let $iframe; const
@ -181,7 +181,7 @@ const helper = {}; // eslint-disable-line
}; };
helper.waitFor = function (conditionFunc, timeoutTime = 1900, intervalTime = 10) { helper.waitFor = function (conditionFunc, timeoutTime = 1900, intervalTime = 10) {
const deferred = $.Deferred(); // eslint-disable-line const deferred = new $.Deferred();
const _fail = deferred.fail.bind(deferred); const _fail = deferred.fail.bind(deferred);
let listenForFail = false; let listenForFail = false;

View file

@ -6,12 +6,12 @@
*/ */
helper.spyOnSocketIO = function () { helper.spyOnSocketIO = function () {
helper.contentWindow().pad.socket.on('message', (msg) => { helper.contentWindow().pad.socket.on('message', (msg) => {
if (msg.type == 'COLLABROOM') { if (msg.type === 'COLLABROOM') {
if (msg.data.type == 'ACCEPT_COMMIT') { if (msg.data.type === 'ACCEPT_COMMIT') {
helper.commits.push(msg); helper.commits.push(msg);
} else if (msg.data.type == 'USER_NEWINFO') { } else if (msg.data.type === 'USER_NEWINFO') {
helper.userInfos.push(msg); helper.userInfos.push(msg);
} else if (msg.data.type == 'CHAT_MESSAGE') { } else if (msg.data.type === 'CHAT_MESSAGE') {
helper.chatMessages.push(msg); helper.chatMessages.push(msg);
} }
} }

View file

@ -1,3 +1,5 @@
'use strict';
/** /**
* the contentWindow is either the normal pad or timeslider * the contentWindow is either the normal pad or timeslider
* *

View file

@ -170,7 +170,7 @@ $(() => {
} }
}); });
// initalize the test helper // initialize the test helper
helper.init(() => { helper.init(() => {
// configure and start the test framework // configure and start the test framework
const grep = getURLParameter('grep'); const grep = getURLParameter('grep');

View file

@ -7,10 +7,11 @@ describe('author of pad edition', function () {
// author 1 creates a new pad with some content (regular lines and lists) // author 1 creates a new pad with some content (regular lines and lists)
before(function (done) { before(function (done) {
var padId = helper.newPad(() => { const padId = helper.newPad(() => {
// make sure pad has at least 3 lines // make sure pad has at least 3 lines
const $firstLine = helper.padInner$('div').first(); const $firstLine = helper.padInner$('div').first();
const threeLines = ['regular line', 'line with ordered list', 'line with unordered list'].join('<br>'); const threeLines = ['regular line', 'line with ordered list', 'line with unordered list']
.join('<br>');
$firstLine.html(threeLines); $firstLine.html(threeLines);
// wait for lines to be processed by Etherpad // wait for lines to be processed by Etherpad
@ -45,7 +46,8 @@ describe('author of pad edition', function () {
setTimeout(() => { setTimeout(() => {
// Expire cookie, so author is changed after reloading the pad. // 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 // 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); helper.newPad(done, padId);
}, 10000); }, 10000);
@ -62,26 +64,25 @@ describe('author of pad edition', function () {
changeLineAndCheckOnlyThatChangeIsFromThisAuthor(REGULAR_LINE, 'x', done); changeLineAndCheckOnlyThatChangeIsFromThisAuthor(REGULAR_LINE, 'x', done);
}); });
it('marks only the new content as changes of the second user on a line with ordered list', function (done) {
it('marks only the new content as changes of the second user on a ' +
'line with ordered list', function (done) {
this.timeout(1000); this.timeout(1000);
changeLineAndCheckOnlyThatChangeIsFromThisAuthor(LINE_WITH_ORDERED_LIST, 'y', done); changeLineAndCheckOnlyThatChangeIsFromThisAuthor(LINE_WITH_ORDERED_LIST, 'y', done);
}); });
it('marks only the new content as changes of the second user on a line with unordered list', function (done) { it('marks only the new content as changes of the second user on ' +
'a line with unordered list', function (done) {
this.timeout(1000); this.timeout(1000);
changeLineAndCheckOnlyThatChangeIsFromThisAuthor(LINE_WITH_UNORDERED_LIST, 'z', done); changeLineAndCheckOnlyThatChangeIsFromThisAuthor(LINE_WITH_UNORDERED_LIST, 'z', done);
}); });
/* ********************** Helper functions ************************ */ /* ********************** Helper functions ************************ */
var getLine = function (lineNumber) { const getLine = (lineNumber) => helper.padInner$('div').eq(lineNumber);
return helper.padInner$('div').eq(lineNumber);
};
const getAuthorFromClassList = function (classes) { const getAuthorFromClassList = (classes) => classes.find((cls) => cls.startsWith('author'));
return classes.find((cls) => cls.startsWith('author'));
};
var changeLineAndCheckOnlyThatChangeIsFromThisAuthor = function (lineNumber, textChange, done) { const changeLineAndCheckOnlyThatChangeIsFromThisAuthor = (lineNumber, textChange, done) => {
// get original author class // get original author class
const classes = getLine(lineNumber).find('span').first().attr('class').split(' '); const classes = getLine(lineNumber).find('span').first().attr('class').split(' ');
const originalAuthor = getAuthorFromClassList(classes); const originalAuthor = getAuthorFromClassList(classes);

View file

@ -22,8 +22,6 @@ describe('bold button', function () {
const $boldButton = chrome$('.buttonicon-bold'); const $boldButton = chrome$('.buttonicon-bold');
$boldButton.click(); $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(); const $newFirstTextElement = inner$('div').first();
// is there a <b> element now? // is there a <b> element now?
@ -48,13 +46,11 @@ describe('bold button', function () {
// select this text element // select this text element
$firstTextElement.sendkeys('{selectall}'); $firstTextElement.sendkeys('{selectall}');
const e = inner$.Event(helper.evtType); const e = new inner$.Event(helper.evtType);
e.ctrlKey = true; // Control key e.ctrlKey = true; // Control key
e.which = 66; // b e.which = 66; // b
inner$('#innerdocbody').trigger(e); 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(); const $newFirstTextElement = inner$('div').first();
// is there a <b> element now? // is there a <b> element now?

View file

@ -1,7 +1,9 @@
'use strict';
describe('As the caret is moved is the UI properly updated?', function () { describe('As the caret is moved is the UI properly updated?', function () {
/*
let padName; let padName;
const numberOfRows = 50; const numberOfRows = 50;
/*
//create a new pad before each test run //create a new pad before each test run
beforeEach(function(cb){ beforeEach(function(cb){
@ -16,7 +18,8 @@ describe('As the caret is moved is the UI properly updated?', function () {
*/ */
/* Tests to do /* 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 (33) / down (34) with and without special keys
* Page up on the first line shouldn't move the viewport * Page up on the first line shouldn't move the viewport
* Down down on the last 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 /* 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? * How can we simulate an edit event in the test framework?
*/ */
/* /*
@ -200,7 +205,8 @@ console.log(inner$);
var chrome$ = helper.padChrome$; var chrome$ = helper.padChrome$;
var numberOfRows = 50; 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 $newFirstTextElement = inner$("div").first();
var originalDivHeight = inner$("div").first().css("height"); var originalDivHeight = inner$("div").first().css("height");
prepareDocument(numberOfRows, $newFirstTextElement); // N lines into the first div as a target 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 helper.waitFor(function(){ // Wait for the DOM to register the new items
return inner$("div").first().text().length == 6; return inner$("div").first().text().length == 6;
}).done(function(){ // Once the DOM has registered the items }).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; var random = Math.floor(Math.random() * (50)) + 20;
$(this).css("height", random+"px"); $(this).css("height", random+"px");
}); });
console.log(caretPosition(inner$)); console.log(caretPosition(inner$));
var newDivHeight = inner$("div").first().css("height"); 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 expect(heightHasChanged).to.be(true); // expect the first line to be blank
}); });
// Is this Element now visible to the pad user? // Is this Element now visible to the pad user?
helper.waitFor(function(){ // Wait for the DOM to register the new items 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 }).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; var random = Math.floor(Math.random() * (80 - 20 + 1)) + 20;
$(this).css("height", random+"px"); $(this).css("height", random+"px");
}); });
var newDivHeight = inner$("div").first().css("height"); 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 expect(heightHasChanged).to.be(true); // expect the first line to be blank
}); });
var i = 0; var i = 0;
@ -241,7 +252,8 @@ console.log(inner$);
// Does scrolling back up the pad with the up arrow show the correct contents? // 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 helper.waitFor(function(){ // Wait for the new position to be in place
try{ 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){ }catch(e){
return false; return false;
} }
@ -256,7 +268,8 @@ console.log(inner$);
// Does scrolling back up the pad with the up arrow show the correct contents? // 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 helper.waitFor(function(){ // Wait for the new position to be in place
try{ 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){ }catch(e){
return false; return false;
} }
@ -276,7 +289,8 @@ console.log(inner$);
// Does scrolling back up the pad with the up arrow show the correct contents? // 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 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 }).done(function(){ // Once the DOM has registered the items
expect(true).to.be(true); expect(true).to.be(true);
done(); 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; let i = 0;
while (i < n) { // for each line while (i < n) { // for each line
target.sendkeys(makeStr()); // generate a random string and send that to the editor target.sendkeys(makeStr()); // generate a random string and send that to the editor
target.sendkeys('{enter}'); // generator an enter keypress target.sendkeys('{enter}'); // generator an enter keypress
i++; // rinse n times i++; // rinse n times
} }
} };
function keyEvent(target, charCode, ctrl, shift) { // sends a charCode to the window // sends a charCode to the window
const e = target.Event(helper.evtType); const keyEvent = (target, charCode, ctrl, shift) => {
const e = new target.Event(helper.evtType);
if (ctrl) { if (ctrl) {
e.ctrlKey = true; // Control key 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.which = charCode;
e.keyCode = charCode; e.keyCode = charCode;
target('#innerdocbody').trigger(e); 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 = ''; let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 5; i++) text += possible.charAt(Math.floor(Math.random() * possible.length)); for (let i = 0; i < 5; i++) text += possible.charAt(Math.floor(Math.random() * possible.length));
return text; 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 docViewTop = $(window).scrollTop();
const docViewBottom = docViewTop + $(window).height(); const docViewBottom = docViewTop + $(window).height();
const elemTop = $(elem).offset().top; // how far the element is from the top of it's container 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.. elemBottom -= 16; // don't ask, sorry but this is needed..
return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop)); return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop));
} };
function caretPosition($) { const caretPosition = ($) => {
const doc = $.window.document; const doc = $.window.document;
const pos = doc.getSelection(); const pos = doc.getSelection();
pos.y = pos.anchorNode.parentElement.offsetTop; pos.y = pos.anchorNode.parentElement.offsetTop;
pos.x = pos.anchorNode.parentElement.offsetLeft; pos.x = pos.anchorNode.parentElement.offsetLeft;
return pos; return pos;
} };

View file

@ -7,7 +7,8 @@ describe('change user color', function () {
this.timeout(60000); 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); this.timeout(10000);
const chrome$ = helper.padChrome$; 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 // simulate a keypress of enter actually does evt.which = 10 not 13
$chatInput.sendkeys('{enter}'); $chatInput.sendkeys('{enter}');
// check if chat shows up
// wait until the chat message shows up // wait until the chat message shows up
helper.waitFor(() => chrome$('#chattext').children('p').length !== 0 helper.waitFor(() => chrome$('#chattext').children('p').length !== 0
).done(() => { ).done(() => {

View file

@ -6,8 +6,10 @@ describe('Chat messages and UI', function () {
helper.newPad(cb); 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); this.timeout(3000);
const chatValue = 'JohnMcLear'; const chatValue = 'JohnMcLear';
await helper.showChat(); await helper.showChat();
@ -49,7 +51,8 @@ describe('Chat messages and UI', function () {
expect(chat.text()).to.be(`${username}${time} ${chatValue}`); 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); this.timeout(5000);
await helper.showSettings(); await helper.showSettings();
@ -66,8 +69,10 @@ describe('Chat messages and UI', function () {
expect(helper.isChatboxShown()).to.be(false); 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); this.timeout(5000);
await helper.showChat(); await helper.showChat();
await helper.enableStickyChatviaIcon(); await helper.enableStickyChatviaIcon();
@ -83,7 +88,8 @@ describe('Chat messages and UI', function () {
expect(helper.isChatboxShown()).to.be(false); 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); this.timeout(60000);
setTimeout(() => { // give it a second to save the username on the server side setTimeout(() => { // give it a second to save the username on the server side

View file

@ -92,7 +92,7 @@ describe('clear authorship colors button', function () {
let hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; let hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1;
expect(hasAuthorClass).to.be(false); expect(hasAuthorClass).to.be(false);
const e = inner$.Event(helper.evtType); const e = new inner$.Event(helper.evtType);
e.ctrlKey = true; // Control key e.ctrlKey = true; // Control key
e.which = 90; // z e.which = 90; // z
inner$('#innerdocbody').trigger(e); // shouldn't od anything inner$('#innerdocbody').trigger(e); // shouldn't od anything

View file

@ -22,8 +22,6 @@ describe('delete keystroke', function () {
$firstTextElement.sendkeys('{leftarrow}'); // simulate a keypress of the left arrow key $firstTextElement.sendkeys('{leftarrow}'); // simulate a keypress of the left arrow key
$firstTextElement.sendkeys('{del}'); // simulate a keypress of delete $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(); const $newFirstTextElement = inner$('div').first();
// get the new length of this element // get the new length of this element

View file

@ -86,16 +86,16 @@ describe('drag and drop', function () {
const TARGET_LINE = 2; const TARGET_LINE = 2;
const FIRST_SOURCE_LINE = 5; const FIRST_SOURCE_LINE = 5;
const getLine = function (lineNumber) { const getLine = (lineNumber) => {
const $lines = helper.padInner$('div'); const $lines = helper.padInner$('div');
return $lines.slice(lineNumber, lineNumber + 1); return $lines.slice(lineNumber, lineNumber + 1);
}; };
const createScriptWithSeveralLines = function (done) { const createScriptWithSeveralLines = (done) => {
// create some lines to be used on the tests // create some lines to be used on the tests
const $firstLine = helper.padInner$('div').first(); const $firstLine = helper.padInner$('div').first();
$firstLine.html( $firstLine.html('...<br>...<br>Target line []<br>...<br>...<br>' +
'...<br>...<br>Target line []<br>...<br>...<br>Source line 1.<br>Source line 2.<br>'); 'Source line 1.<br>Source line 2.<br>');
// wait for lines to be split // wait for lines to be split
helper.waitFor(() => { helper.waitFor(() => {
@ -104,7 +104,7 @@ describe('drag and drop', function () {
}).done(done); }).done(done);
}; };
const selectPartOfSourceLine = function () { const selectPartOfSourceLine = () => {
const $sourceLine = getLine(FIRST_SOURCE_LINE); const $sourceLine = getLine(FIRST_SOURCE_LINE);
// select 'line 1' from 'Source line 1.' // select 'line 1' from 'Source line 1.'
@ -112,14 +112,14 @@ describe('drag and drop', function () {
const end = start + 'line 1'.length; const end = start + 'line 1'.length;
helper.selectLines($sourceLine, $sourceLine, start, end); helper.selectLines($sourceLine, $sourceLine, start, end);
}; };
const selectMultipleSourceLines = function () { const selectMultipleSourceLines = () => {
const $firstSourceLine = getLine(FIRST_SOURCE_LINE); const $firstSourceLine = getLine(FIRST_SOURCE_LINE);
const $lastSourceLine = getLine(FIRST_SOURCE_LINE + 1); const $lastSourceLine = getLine(FIRST_SOURCE_LINE + 1);
helper.selectLines($firstSourceLine, $lastSourceLine); helper.selectLines($firstSourceLine, $lastSourceLine);
}; };
const dragSelectedTextAndDropItIntoMiddleOfLine = function (targetLineNumber) { const dragSelectedTextAndDropItIntoMiddleOfLine = (targetLineNumber) => {
// dragstart: start dragging content // dragstart: start dragging content
triggerEvent('dragstart'); triggerEvent('dragstart');
@ -132,7 +132,7 @@ describe('drag and drop', function () {
triggerEvent('dragend'); triggerEvent('dragend');
}; };
const getHtmlFromSelectedText = function () { const getHtmlFromSelectedText = () => {
const innerDocument = helper.padInner$.document; const innerDocument = helper.padInner$.document;
const range = innerDocument.getSelection().getRangeAt(0); const range = innerDocument.getSelection().getRangeAt(0);
@ -145,12 +145,12 @@ describe('drag and drop', function () {
return draggedHtml; return draggedHtml;
}; };
const triggerEvent = function (eventName) { const triggerEvent = (eventName) => {
const event = helper.padInner$.Event(eventName); const event = new helper.padInner$.Event(eventName);
helper.padInner$('#innerdocbody').trigger(event); helper.padInner$('#innerdocbody').trigger(event);
}; };
const moveSelectionIntoTarget = function (draggedHtml, targetLineNumber) { const moveSelectionIntoTarget = (draggedHtml, targetLineNumber) => {
const innerDocument = helper.padInner$.document; const innerDocument = helper.padInner$.document;
// delete original content // delete original content

View file

@ -7,7 +7,7 @@ describe('the test helper', function () {
let times = 10; let times = 10;
const loadPad = function () { const loadPad = () => {
helper.newPad(() => { helper.newPad(() => {
times--; times--;
if (times > 0) { if (times > 0) {
@ -77,13 +77,14 @@ describe('the test helper', function () {
// Before refreshing, make sure the name is there // Before refreshing, make sure the name is there
expect($usernameInput.val()).to.be('John McLear'); 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'; chrome$.document.cookie = 'prefsHtml=baz;expires=Thu, 01 Jan 3030 00:00:00 GMT';
expect(chrome$.document.cookie).to.contain('prefsHtml=baz'); expect(chrome$.document.cookie).to.contain('prefsHtml=baz');
// Cookies are weird. Because it's attached to chrome$ (as helper.setPadCookies does), AND we // Cookies are weird. Because it's attached to chrome$ (as helper.setPadCookies does)
// didn't put path=/, we shouldn't expect it to be visible on window.document.cookie. Let's just // AND we didn't put path=/, we shouldn't expect it to be visible on
// be sure. // window.document.cookie. Let's just be sure.
expect(window.document.cookie).to.not.contain('prefsHtml=baz'); expect(window.document.cookie).to.not.contain('prefsHtml=baz');
setTimeout(() => { // give it a second to save the username on the server side 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); 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 inner$ = helper.padInner$;
const startOffset = 2; 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 * is not consistent between browsers but that's the situation so that's
* how I'm covering it in this test. * 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(); done();
}); });
@ -367,12 +370,14 @@ describe('the test helper', function () {
* is not consistent between browsers but that's the situation so that's * is not consistent between browsers but that's the situation so that's
* how I'm covering it in this test. * 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(); 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 inner$ = helper.padInner$;
const $lines = inner$('div'); 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 * is not consistent between browsers but that's the situation so that's
* how I'm covering it in this test. * 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(); done();
}); });

View file

@ -1,3 +1,5 @@
'use strict';
describe('import functionality', function () { describe('import functionality', function () {
beforeEach(function (cb) { beforeEach(function (cb) {
helper.newPad(cb); // creates a new pad helper.newPad(cb); // creates a new pad
@ -16,7 +18,6 @@ describe('import functionality', function () {
return newtext; return newtext;
} }
function importrequest(data, importurl, type) { function importrequest(data, importurl, type) {
let success;
let error; let error;
const result = $.ajax({ const result = $.ajax({
url: importurl, url: importurl,
@ -27,7 +28,17 @@ describe('import functionality', function () {
accepts: { accepts: {
text: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 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) {
error = res; error = res;
}, },
@ -56,7 +67,8 @@ describe('import functionality', function () {
const importurl = `${helper.padChrome$.window.location.href}/import`; const importurl = `${helper.padChrome$.window.location.href}/import`;
const textWithNewLines = 'imported text\nnewline'; const textWithNewLines = 'imported text\nnewline';
importrequest(textWithNewLines, importurl, 'txt'); importrequest(textWithNewLines, importurl, 'txt');
helper.waitFor(() => expect(getinnertext()).to.be('<span class="">imported text</span>\n<span class="">newline</span>\n<br>\n')); helper.waitFor(() => expect(getinnertext())
.to.be('<span class="">imported text</span>\n<span class="">newline</span>\n<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href); const results = exportfunc(helper.padChrome$.window.location.href);
expect(results[0][1]).to.be('imported text<br>newline<br><br>'); expect(results[0][1]).to.be('imported text<br>newline<br><br>');
expect(results[1][1]).to.be('imported text\nnewline\n\n'); 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 importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithNewLines = '<html><body>htmltext<br/>newline</body></html>'; const htmlWithNewLines = '<html><body>htmltext<br/>newline</body></html>';
importrequest(htmlWithNewLines, importurl, 'html'); importrequest(htmlWithNewLines, importurl, 'html');
helper.waitFor(() => expect(getinnertext()).to.be('<span class="">htmltext</span>\n<span class="">newline</span>\n<br>\n')); helper.waitFor(() => expect(getinnertext())
.to.be('<span class="">htmltext</span>\n<span class="">newline</span>\n<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href); const results = exportfunc(helper.padChrome$.window.location.href);
expect(results[0][1]).to.be('htmltext<br>newline<br><br>'); expect(results[0][1]).to.be('htmltext<br>newline<br><br>');
expect(results[1][1]).to.be('htmltext\nnewline\n\n'); 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) { xit('import a pad with attributes from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`; const importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithNewLines = '<html><body>htmltext<br/><span class="b s i u"><b><i><s><u>newline</u></s></i></b></body></html>'; const htmlWithNewLines = '<html><body>htmltext<br/><span class="b s i u">' +
'<b><i><s><u>newline</u></s></i></b></body></html>';
importrequest(htmlWithNewLines, importurl, 'html'); importrequest(htmlWithNewLines, importurl, 'html');
helper.waitFor(() => expect(getinnertext()).to.be('<span class="">htmltext</span>\n<span class="b i s u"><b><i><s><u>newline</u></s></i></b></span>\n<br>\n')); helper.waitFor(() => expect(getinnertext())
.to.be('<span class="">htmltext</span>\n<span class="b i s u">' +
'<b><i><s><u>newline</u></s></i></b></span>\n<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href); const results = exportfunc(helper.padChrome$.window.location.href);
expect(results[0][1]).to.be('htmltext<br><strong><em><s><u>newline</u></s></em></strong><br><br>'); expect(results[0][1])
.to.be('htmltext<br><strong><em><s><u>newline</u></s></em></strong><br><br>');
expect(results[1][1]).to.be('htmltext\nnewline\n\n'); expect(results[1][1]).to.be('htmltext\nnewline\n\n');
done(); done();
}); });
xit('import a pad with bullets from html', function (done) { xit('import a pad with bullets from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`; const importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithBullets = '<html><body><ul class="list-bullet1"><li>bullet line 1</li><li>bullet line 2</li><ul class="list-bullet2"><li>bullet2 line 1</li><li>bullet2 line 2</li></ul></ul></body></html>'; const htmlWithBullets = '<html><body><ul class="list-bullet1"><li>bullet line 1</li>' +
'<li>bullet line 2</li><ul class="list-bullet2"><li>bullet2 line 1</li>' +
'<li>bullet2 line 2</li></ul></ul></body></html>';
importrequest(htmlWithBullets, importurl, 'html'); importrequest(htmlWithBullets, importurl, 'html');
helper.waitFor(() => expect(getinnertext()).to.be('\ helper.waitFor(() => expect(getinnertext()).to.be(
<ul class="list-bullet1"><li><span class="">bullet line 1</span></li></ul>\n\ '<ul class="list-bullet1"><li><span class="">bullet line 1</span></li></ul>\n' +
<ul class="list-bullet1"><li><span class="">bullet line 2</span></li></ul>\n\ '<ul class="list-bullet1"><li><span class="">bullet line 2</span></li></ul>\n' +
<ul class="list-bullet2"><li><span class="">bullet2 line 1</span></li></ul>\n\ '<ul class="list-bullet2"><li><span class="">bullet2 line 1</span></li></ul>\n' +
<ul class="list-bullet2"><li><span class="">bullet2 line 2</span></li></ul>\n\ '<ul class="list-bullet2"><li><span class="">bullet2 line 2</span></li></ul>\n' +
<br>\n')); '<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href); const results = exportfunc(helper.padChrome$.window.location.href);
expect(results[0][1]).to.be('<ul class="bullet"><li>bullet line 1</li><li>bullet line 2</li><ul class="bullet"><li>bullet2 line 1</li><li>bullet2 line 2</li></ul></ul><br>'); expect(results[0][1]).to.be(
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'); '<ul class="bullet"><li>bullet line 1</li><li>bullet line 2</li>' +
'<ul class="bullet"><li>bullet2 line 1</li><li>bullet2 line 2</li></ul></ul><br>');
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(); done();
}); });
xit('import a pad with bullets and newlines from html', function (done) { xit('import a pad with bullets and newlines from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`; const importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithBullets = '<html><body><ul class="list-bullet1"><li>bullet line 1</li></ul><br/><ul class="list-bullet1"><li>bullet line 2</li><ul class="list-bullet2"><li>bullet2 line 1</li></ul></ul><br/><ul class="list-bullet1"><ul class="list-bullet2"><li>bullet2 line 2</li></ul></ul></body></html>'; const htmlWithBullets = '<html><body><ul class="list-bullet1"><li>bullet line 1</li>' +
'</ul><br/><ul class="list-bullet1"><li>bullet line 2</li><ul class="list-bullet2">' +
'<li>bullet2 line 1</li></ul></ul><br/><ul class="list-bullet1">' +
'<ul class="list-bullet2"><li>bullet2 line 2</li></ul></ul></body></html>';
importrequest(htmlWithBullets, importurl, 'html'); importrequest(htmlWithBullets, importurl, 'html');
helper.waitFor(() => expect(getinnertext()).to.be('\ helper.waitFor(() => expect(getinnertext()).to.be(
<ul class="list-bullet1"><li><span class="">bullet line 1</span></li></ul>\n\ '<ul class="list-bullet1"><li><span class="">bullet line 1</span></li></ul>\n' +
<br>\n\ '<br>\n' +
<ul class="list-bullet1"><li><span class="">bullet line 2</span></li></ul>\n\ '<ul class="list-bullet1"><li><span class="">bullet line 2</span></li></ul>\n' +
<ul class="list-bullet2"><li><span class="">bullet2 line 1</span></li></ul>\n\ '<ul class="list-bullet2"><li><span class="">bullet2 line 1</span></li></ul>\n' +
<br>\n\ '<br>\n' +
<ul class="list-bullet2"><li><span class="">bullet2 line 2</span></li></ul>\n\ '<ul class="list-bullet2"><li><span class="">bullet2 line 2</span></li></ul>\n' +
<br>\n')); '<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href); const results = exportfunc(helper.padChrome$.window.location.href);
expect(results[0][1]).to.be('<ul class="bullet"><li>bullet line 1</li></ul><br><ul class="bullet"><li>bullet line 2</li><ul class="bullet"><li>bullet2 line 1</li></ul></ul><br><ul><ul class="bullet"><li>bullet2 line 2</li></ul></ul><br>'); expect(results[0][1]).to.be(
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'); '<ul class="bullet"><li>bullet line 1</li></ul><br><ul class="bullet">' +
'<li>bullet line 2</li><ul class="bullet"><li>bullet2 line 1</li></ul>' +
'</ul><br><ul><ul class="bullet"><li>bullet2 line 2</li></ul></ul><br>');
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(); done();
}); });
xit('import a pad with bullets and newlines and attributes from html', function (done) { xit('import a pad with bullets and newlines and attributes from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`; const importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithBullets = '<html><body><ul class="list-bullet1"><li>bullet line 1</li></ul><br/><ul class="list-bullet1"><li>bullet line 2</li><ul class="list-bullet2"><li>bullet2 line 1</li></ul></ul><br/><ul class="list-bullet1"><ul class="list-bullet2"><ul class="list-bullet3"><ul class="list-bullet4"><li><span class="b s i u"><b><i><s><u>bullet4 line 2 bisu</u></s></i></b></span></li><li><span class="b s "><b><s>bullet4 line 2 bs</s></b></span></li><li><span class="u"><u>bullet4 line 2 u</u></span><span class="u i s"><i><s><u>uis</u></s></i></span></li></ul></ul></ul></ul></body></html>'; const htmlWithBullets = '<html><body><ul class="list-bullet1"><li>bullet line 1</li>' +
'</ul><br/><ul class="list-bullet1"><li>bullet line 2</li>' +
'<ul class="list-bullet2"><li>bullet2 line 1</li></ul></ul>' +
'<br/><ul class="list-bullet1"><ul class="list-bullet2"><ul class="list-bullet3">' +
'<ul class="list-bullet4"><li><span class="b s i u"><b><i>' +
'<s><u>bullet4 line 2 bisu</u></s></i></b></span></li><li>' +
'<span class="b s "><b><s>bullet4 line 2 bs</s></b></span></li>' +
'<li><span class="u"><u>bullet4 line 2 u</u></span><span class="u i s">' +
'<i><s><u>uis</u></s></i></span></li></ul></ul></ul></ul></body></html>';
importrequest(htmlWithBullets, importurl, 'html'); importrequest(htmlWithBullets, importurl, 'html');
helper.waitFor(() => expect(getinnertext()).to.be('\ helper.waitFor(() => expect(getinnertext()).to.be(
<ul class="list-bullet1"><li><span class="">bullet line 1</span></li></ul>\n\<br>\n\ '<ul class="list-bullet1"><li><span class="">bullet line 1</span></li></ul>\n<br>\n' +
<ul class="list-bullet1"><li><span class="">bullet line 2</span></li></ul>\n\ '<ul class="list-bullet1"><li><span class="">bullet line 2</span></li></ul>\n' +
<ul class="list-bullet2"><li><span class="">bullet2 line 1</span></li></ul>\n<br>\n\ '<ul class="list-bullet2"><li><span class="">bullet2 line 1</span></li></ul>\n<br>\n' +
<ul class="list-bullet4"><li><span class="b i s u"><b><i><s><u>bullet4 line 2 bisu</u></s></i></b></span></li></ul>\n\ '<ul class="list-bullet4"><li><span class="b i s u">' +
<ul class="list-bullet4"><li><span class="b s"><b><s>bullet4 line 2 bs</s></b></span></li></ul>\n\ '<b><i><s><u>bullet4 line 2 bisu</u></s></i></b></span></li></ul>\n' +
<ul class="list-bullet4"><li><span class="u"><u>bullet4 line 2 u</u></span><span class="i s u"><i><s><u>uis</u></s></i></span></li></ul>\n\ '<ul class="list-bullet4"><li><span class="b s">' +
<br>\n')); '<b><s>bullet4 line 2 bs</s></b></span></li></ul>\n' +
'<ul class="list-bullet4"><li><span class="u"><u>bullet4 line 2 u</u>' +
'</span><span class="i s u"><i><s><u>uis</u></s></i></span></li></ul>\n' +
'<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href); const results = exportfunc(helper.padChrome$.window.location.href);
expect(results[0][1]).to.be('<ul class="bullet"><li>bullet line 1</li></ul><br><ul class="bullet"><li>bullet line 2</li><ul class="bullet"><li>bullet2 line 1</li></ul></ul><br><ul><ul><ul><ul class="bullet"><li><strong><em><s><u>bullet4 line 2 bisu</u></s></em></strong></li><li><strong><s>bullet4 line 2 bs</s></strong></li><li><u>bullet4 line 2 u<em><s>uis</s></em></u></li></ul></ul></ul></ul><br>'); expect(results[0][1]).to.be(
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'); '<ul class="bullet"><li>bullet line 1</li></ul>' +
'<br><ul class="bullet"><li>bullet line 2</li><ul class="bullet"><li>bullet2 line 1</li>' +
'</ul></ul><br><ul><ul><ul><ul class="bullet"><li><strong><em><s><u>bullet4 line 2 bisu' +
'</u></s></em></strong></li><li><strong><s>bullet4 line 2 bs</s></strong>' +
'</li><li><u>bullet4 line 2 u<em><s>uis</s></em></u></li></ul></ul></ul></ul><br>');
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(); done();
}); });
xit('import a pad with nested bullets from html', function (done) { xit('import a pad with nested bullets from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`; const importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithBullets = '<html><body><ul class="list-bullet1"><li>bullet line 1</li></ul><ul class="list-bullet1"><li>bullet line 2</li><ul class="list-bullet2"><li>bullet2 line 1</li></ul></ul><ul class="list-bullet1"><ul class="list-bullet2"><ul class="list-bullet3"><ul class="list-bullet4"><li>bullet4 line 2</li><li>bullet4 line 2</li><li>bullet4 line 2</li></ul><li>bullet3 line 1</li></ul></ul><li>bullet2 line 1</li></ul></body></html>'; const htmlWithBullets = '<html><body><ul class="list-bullet1"><li>bullet line 1</li>' +
'</ul><ul class="list-bullet1"><li>bullet line 2</li><ul class="list-bullet2">' +
'<li>bullet2 line 1</li></ul></ul><ul class="list-bullet1"><ul class="list-bullet2">' +
'<ul class="list-bullet3"><ul class="list-bullet4"><li>bullet4 line 2</li>' +
'<li>bullet4 line 2</li><li>bullet4 line 2</li></ul><li>bullet3 line 1</li></ul>' +
'</ul><li>bullet2 line 1</li></ul></body></html>';
importrequest(htmlWithBullets, importurl, 'html'); importrequest(htmlWithBullets, importurl, 'html');
const oldtext = getinnertext(); const oldtext = getinnertext();
helper.waitFor(() => oldtext != getinnertext() helper.waitFor(() => oldtext !== getinnertext()
// return expect(getinnertext()).to.be('\ // return expect(getinnertext()).to.be('\
// <ul class="list-bullet1"><li><span class="">bullet line 1</span></li></ul>\n\ // <ul class="list-bullet1"><li><span class="">bullet line 1</span></li></ul>\n\
// <ul class="list-bullet1"><li><span class="">bullet line 2</span></li></ul>\n\ // <ul class="list-bullet1"><li><span class="">bullet line 2</span></li></ul>\n\
@ -148,73 +201,127 @@ describe('import functionality', function () {
); );
const results = exportfunc(helper.padChrome$.window.location.href); const results = exportfunc(helper.padChrome$.window.location.href);
expect(results[0][1]).to.be('<ul class="bullet"><li>bullet line 1</li><li>bullet line 2</li><ul class="bullet"><li>bullet2 line 1</li><ul><ul class="bullet"><li>bullet4 line 2</li><li>bullet4 line 2</li><li>bullet4 line 2</li></ul><li>bullet3 line 1</li></ul></ul><li>bullet2 line 1</li></ul><br>'); expect(results[0][1]).to.be(
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'); '<ul class="bullet"><li>bullet line 1</li><li>bullet line 2</li>' +
'<ul class="bullet"><li>bullet2 line 1</li><ul><ul class="bullet"><li>bullet4 line 2</li>' +
'<li>bullet4 line 2</li><li>bullet4 line 2</li></ul><li>bullet3 line 1</li></ul></ul>' +
'<li>bullet2 line 1</li></ul><br>');
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(); 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 importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithBullets = '<html><body><ul class="list-bullet1"><li>bullet line 1</li></ul><br/><ul class="list-bullet1"><li>bullet line 2</li><ul class="list-bullet2"><li>bullet2 line 1</li></ul></ul><br/><ul class="list-bullet1"><ul class="list-bullet2"><ul class="list-bullet3"><ul class="list-bullet4"><li><span class="b s i u"><b><i><s><u>bullet4 line 2 bisu</u></s></i></b></span></li><li><span class="b s "><b><s>bullet4 line 2 bs</s></b></span></li><li><span class="u"><u>bullet4 line 2 u</u></span><span class="u i s"><i><s><u>uis</u></s></i></span></li><ul class="list-bullet5"><ul class="list-bullet6"><ul class="list-bullet7"><ul class="list-bullet8"><li><span class="">foo</span></li><li><span class="b s"><b><s>foobar bs</b></s></span></li></ul></ul></ul></ul><ul class="list-bullet5"><li>foobar</li></ul></ul></ul></ul></body></html>'; const htmlWithBullets =
'<html><body><ul class="list-bullet1"><li>bullet line 1</li>' +
'</ul><br/><ul class="list-bullet1"><li>bullet line 2</li><ul class="list-bullet2"><li>' +
'bullet2 line 1</li></ul></ul><br/><ul class="list-bullet1"><ul class="list-bullet2">' +
'<ul class="list-bullet3"><ul class="list-bullet4"><li><span class="b s i u"><b><i>' +
'<s><u>bullet4 line 2 bisu</u></s></i></b></span></li><li><span class="b s "><b><s>' +
'bullet4 line 2 bs</s></b></span></li><li><span class="u"><u>bullet4 line 2 u' +
'</u></span><span class="u i s"><i><s><u>uis</u></s></i></span></li>' +
'<ul class="list-bullet5"><ul class="list-bullet6"><ul class="list-bullet7">' +
'<ul class="list-bullet8"><li><span class="">foo</span></li><li><span class="b s">' +
'<b><s>foobar bs</b></s></span></li></ul></ul></ul></ul><ul class="list-bullet5">' +
'<li>foobar</li></ul></ul></ul></ul></body></html>';
importrequest(htmlWithBullets, importurl, 'html'); importrequest(htmlWithBullets, importurl, 'html');
helper.waitFor(() => expect(getinnertext()).to.be('\ helper.waitFor(() => expect(getinnertext()).to.be(
<ul class="list-bullet1"><li><span class="">bullet line 1</span></li></ul>\n\<br>\n\ '<ul class="list-bullet1"><li><span class="">bullet line 1</span></li></ul>\n<br>\n' +
<ul class="list-bullet1"><li><span class="">bullet line 2</span></li></ul>\n\ '<ul class="list-bullet1"><li><span class="">bullet line 2</span></li></ul>\n' +
<ul class="list-bullet2"><li><span class="">bullet2 line 1</span></li></ul>\n<br>\n\ '<ul class="list-bullet2"><li><span class="">bullet2 line 1</span></li></ul>\n<br>\n' +
<ul class="list-bullet4"><li><span class="b i s u"><b><i><s><u>bullet4 line 2 bisu</u></s></i></b></span></li></ul>\n\ '<ul class="list-bullet4"><li><span class="b i s u"><b><i><s><u>bullet4 line 2 bisu</u>' +
<ul class="list-bullet4"><li><span class="b s"><b><s>bullet4 line 2 bs</s></b></span></li></ul>\n\ '</s></i></b></span></li></ul>\n' +
<ul class="list-bullet4"><li><span class="u"><u>bullet4 line 2 u</u></span><span class="i s u"><i><s><u>uis</u></s></i></span></li></ul>\n\ '<ul class="list-bullet4"><li><span class="b s"><b><s>bullet4 line 2 bs</s></b>' +
<ul class="list-bullet8"><li><span class="">foo</span></li></ul>\n\ '</span></li></ul>\n' +
<ul class="list-bullet8"><li><span class="b s"><b><s>foobar bs</s></b></span></li></ul>\n\ '<ul class="list-bullet4"><li><span class="u"><u>bullet4 line 2 u</u></span>' +
<ul class="list-bullet5"><li><span class="">foobar</span></li></ul>\n\ '<span class="i s u"><i><s><u>uis</u></s>' +
<br>\n')); '</i></span></li></ul>\n' +
'<ul class="list-bullet8"><li><span class="">foo</span></li></ul>\n' +
'<ul class="list-bullet8"><li><span class="b s"><b><s>foobar bs</s></b>' +
'</span></li></ul>\n' +
'<ul class="list-bullet5"><li><span class="">foobar</span></li></ul>\n' +
'<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href); const results = exportfunc(helper.padChrome$.window.location.href);
expect(results[0][1]).to.be('<ul class="bullet"><li>bullet line 1</li></ul><br><ul class="bullet"><li>bullet line 2</li><ul class="bullet"><li>bullet2 line 1</li></ul></ul><br><ul><ul><ul><ul class="bullet"><li><strong><em><s><u>bullet4 line 2 bisu</u></s></em></strong></li><li><strong><s>bullet4 line 2 bs</s></strong></li><li><u>bullet4 line 2 u<em><s>uis</s></em></u></li><ul><ul><ul><ul class="bullet"><li>foo</li><li><strong><s>foobar bs</s></strong></li></ul></ul></ul><li>foobar</li></ul></ul></ul></ul></ul><br>'); expect(results[0][1]).to.be(
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'); '<ul class="bullet"><li>bullet line 1</li></ul><br><ul class="bullet">' +
'<li>bullet line 2</li><ul class="bullet"><li>bullet2 line 1</li></ul></ul>' +
'<br><ul><ul><ul><ul class="bullet"><li><strong><em><s><u>' +
'bullet4 line 2 bisu</u></s></em></strong></li><li><strong><s>' +
'bullet4 line 2 bs</s></strong></li><li><u>bullet4 line 2 u<em>' +
'<s>uis</s></em></u></li><ul><ul><ul><ul class="bullet"><li>foo</li>' +
'<li><strong><s>foobar bs</s></strong></li></ul></ul></ul><li>foobar</li>' +
'</ul></ul></ul></ul></ul><br>');
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(); done();
}); });
xit('import a pad with ordered lists from html', function (done) { xit('import a pad with ordered lists from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`; const importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithBullets = '<html><body><ol class="list-number1" start="1"><li>number 1 line 1</li></ol><ol class="list-number1" start="2"><li>number 2 line 2</li></ol></body></html>'; const htmlWithBullets = '<html><body><ol class="list-number1" start="1">' +
'<li>number 1 line 1</li></ol><ol class="list-number1" start="2">' +
'<li>number 2 line 2</li></ol></body></html>';
importrequest(htmlWithBullets, importurl, 'html'); importrequest(htmlWithBullets, importurl, 'html');
console.error(getinnertext()); console.error(getinnertext());
expect(getinnertext()).to.be('\ expect(getinnertext()).to.be(
<ol class="list-number1" start="1"><li><span class="">number 1 line 1</span></li></ol>\n\ '<ol class="list-number1" start="1"><li><span class="">number 1 line 1</span></li></ol>\n' +
<ol class="list-number1" start="2"><li><span class="">number 2 line 2</span></li></ol>\n\ '<ol class="list-number1" start="2"><li><span class="">number 2 line 2</span></li></ol>\n' +
<br>\n'); '<br>\n');
const results = exportfunc(helper.padChrome$.window.location.href); const results = exportfunc(helper.padChrome$.window.location.href);
expect(results[0][1]).to.be('<ol class="list-number1" start="1"><li>number 1 line 1</li></ol><ol class="list-number1" start="2"><li>number 2 line 2</li></ol>'); expect(results[0][1]).to.be(
'<ol class="list-number1" start="1"><li>number 1 line 1</li>' +
'</ol><ol class="list-number1" start="2"><li>number 2 line 2</li></ol>');
expect(results[1][1]).to.be(''); expect(results[1][1]).to.be('');
done(); done();
}); });
xit('import a pad with ordered lists and newlines from html', function (done) { xit('import a pad with ordered lists and newlines from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`; const importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithBullets = '<html><body><ol class="list-number1" start="1"><li>number 9 line 1</li></ol><br/><ol class="list-number1" start="2"><li>number 10 line 2</li><ol class="list-number2"><li>number 2 times line 1</li></ol></ol><br/><ol class="list-bullet1"><ol class="list-number2"><li>number 2 times line 2</li></ol></ol></body></html>'; const htmlWithBullets = '<html><body><ol class="list-number1" start="1">' +
'<li>number 9 line 1</li></ol><br/><ol class="list-number1" start="2">' +
'<li>number 10 line 2</li><ol class="list-number2">' +
'<li>number 2 times line 1</li></ol></ol><br/><ol class="list-bullet1">' +
'<ol class="list-number2"><li>number 2 times line 2</li></ol></ol></body></html>';
importrequest(htmlWithBullets, importurl, 'html'); importrequest(htmlWithBullets, importurl, 'html');
expect(getinnertext()).to.be('\ expect(getinnertext()).to.be(
<ol class="list-number1" start="1"><li><span class="">number 9 line 1</span></li></ol>\n\ '<ol class="list-number1" start="1"><li><span class="">number 9 line 1</span></li></ol>\n' +
<br>\n\ '<br>\n' +
<ol class="list-number1" start="2"><li><span class="">number 10 line 2</span></li></ol>\n\ '<ol class="list-number1" start="2"><li><span class="">number 10 line 2</span></li>' +
<ol class="list-number2"><li><span class="">number 2 times line 1</span></li></ol>\n\ '</ol>\n' +
<br>\n\ '<ol class="list-number2"><li><span class="">number 2 times line 1</span></li></ol>\n' +
<ol class="list-number2"><li><span class="">number 2 times line 2</span></li></ol>\n\ '<br>\n' +
<br>\n'); '<ol class="list-number2"><li><span class="">number 2 times line 2</span></li></ol>\n' +
'<br>\n');
const results = exportfunc(helper.padChrome$.window.location.href); const results = exportfunc(helper.padChrome$.window.location.href);
console.error(results); console.error(results);
done(); 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 importurl = `${helper.padChrome$.window.location.href}/import`;
const htmlWithBullets = '<html><body><ol class="list-number1" start="1"><li><span class="b s i u"><b><i><s><u>bold strikethrough italics underline</u></s><i/></b></span> line <span class="b"><b>1bold</b></span></li></ol><br/><span class="i"><i><ol class="list-number1" start="2"><li>number 10 line 2</li><ol class="list-number2"><li>number 2 times line 1</li></ol></ol></i></span><br/><ol class="list-bullet1"><ol class="list-number2"><li>number 2 times line 2</li></ol></ol></body></html>'; const htmlWithBullets = '<html><body><ol class="list-number1" start="1"><li>' +
'<span class="b s i u"><b><i><s><u>bold strikethrough italics underline</u>' +
'</s><i/></b></span> line <span class="b"><b>1bold</b></span></li>' +
'</ol><br/><span class="i"><i><ol class="list-number1" start="2">' +
'<li>number 10 line 2</li><ol class="list-number2">' +
'<li>number 2 times line 1</li></ol></ol></i></span><br/>' +
'<ol class="list-bullet1"><ol class="list-number2">' +
'<li>number 2 times line 2</li></ol></ol></body></html>';
importrequest(htmlWithBullets, importurl, 'html'); importrequest(htmlWithBullets, importurl, 'html');
expect(getinnertext()).to.be('\ expect(getinnertext()).to.be(
<ol class="list-number1"><li><span class="b i s u"><b><i><s><u>bold strikethrough italics underline</u></s></i></b></span><span class=""> line </span><span class="b"><b>1bold</b></span></li></ol>\n\ '<ol class="list-number1"><li><span class="b i s u"><b><i><s><u>' +
<br>\n\ 'bold strikethrough italics underline</u></s></i></b></span><span class="">' +
<ol class="list-number1"><li><span class="i"><i>number 10 line 2</i></span></li></ol>\n\ ' line </span><span class="b"><b>1bold</b></span></li></ol>\n' +
<ol class="list-number2"><li><span class="i"><i>number 2 times line 1</i></span></li></ol>\n\ '<br>\n' +
<br>\n\ '<ol class="list-number1"><li><span class="i"><i>number 10 line 2</i></span></li></ol>\n' +
<ol class="list-number2"><li><span class="">number 2 times line 2</span></li></ol>\n\ '<ol class="list-number2"><li><span class="i">' +
<br>\n'); '<i>number 2 times line 1</i></span></li></ol>\n' +
'<br>\n' +
'<ol class="list-number2"><li><span class="">number 2 times line 2</span></li></ol>\n' +
'<br>\n');
const results = exportfunc(helper.padChrome$.window.location.href); const results = exportfunc(helper.padChrome$.window.location.href);
console.error(results); console.error(results);
done(); done();

View file

@ -1,3 +1,5 @@
'use strict';
describe('import indents functionality', function () { describe('import indents functionality', function () {
beforeEach(function (cb) { beforeEach(function (cb) {
helper.newPad(cb); // creates a new pad helper.newPad(cb); // creates a new pad
@ -13,7 +15,6 @@ describe('import indents functionality', function () {
return newtext; return newtext;
} }
function importrequest(data, importurl, type) { function importrequest(data, importurl, type) {
let success;
let error; let error;
const result = $.ajax({ const result = $.ajax({
url: importurl, url: importurl,
@ -24,7 +25,17 @@ describe('import indents functionality', function () {
accepts: { accepts: {
text: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 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) {
error = res; error = res;
}, },
@ -51,54 +62,67 @@ describe('import indents functionality', function () {
xit('import a pad with indents from html', function (done) { xit('import a pad with indents from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`; const importurl = `${helper.padChrome$.window.location.href}/import`;
/* eslint-disable-next-line max-len */
const htmlWithIndents = '<html><body><ul class="list-indent1"><li>indent line 1</li><li>indent line 2</li><ul class="list-indent2"><li>indent2 line 1</li><li>indent2 line 2</li></ul></ul></body></html>'; const htmlWithIndents = '<html><body><ul class="list-indent1"><li>indent line 1</li><li>indent line 2</li><ul class="list-indent2"><li>indent2 line 1</li><li>indent2 line 2</li></ul></ul></body></html>';
importrequest(htmlWithIndents, importurl, 'html'); importrequest(htmlWithIndents, importurl, 'html');
helper.waitFor(() => expect(getinnertext()).to.be('\ helper.waitFor(() => expect(getinnertext()).to.be(
<ul class="list-indent1"><li><span class="">indent line 1</span></li></ul>\n\ '<ul class="list-indent1"><li><span class="">indent line 1</span></li></ul>\n' +
<ul class="list-indent1"><li><span class="">indent line 2</span></li></ul>\n\ '<ul class="list-indent1"><li><span class="">indent line 2</span></li></ul>\n' +
<ul class="list-indent2"><li><span class="">indent2 line 1</span></li></ul>\n\ '<ul class="list-indent2"><li><span class="">indent2 line 1</span></li></ul>\n' +
<ul class="list-indent2"><li><span class="">indent2 line 2</span></li></ul>\n\ '<ul class="list-indent2"><li><span class="">indent2 line 2</span></li></ul>\n' +
<br>\n')); '<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href); const results = exportfunc(helper.padChrome$.window.location.href);
/* eslint-disable-next-line max-len */
expect(results[0][1]).to.be('<ul class="indent"><li>indent line 1</li><li>indent line 2</li><ul class="indent"><li>indent2 line 1</li><li>indent2 line 2</li></ul></ul><br>'); expect(results[0][1]).to.be('<ul class="indent"><li>indent line 1</li><li>indent line 2</li><ul class="indent"><li>indent2 line 1</li><li>indent2 line 2</li></ul></ul><br>');
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(); done();
}); });
xit('import a pad with indented lists and newlines from html', function (done) { xit('import a pad with indented lists and newlines from html', function (done) {
const importurl = `${helper.padChrome$.window.location.href}/import`; const importurl = `${helper.padChrome$.window.location.href}/import`;
/* eslint-disable-next-line max-len */
const htmlWithIndents = '<html><body><ul class="list-indent1"><li>indent line 1</li></ul><br/><ul class="list-indent1"><li>indent 1 line 2</li><ul class="list-indent2"><li>indent 2 times line 1</li></ul></ul><br/><ul class="list-indent1"><ul class="list-indent2"><li>indent 2 times line 2</li></ul></ul></body></html>'; const htmlWithIndents = '<html><body><ul class="list-indent1"><li>indent line 1</li></ul><br/><ul class="list-indent1"><li>indent 1 line 2</li><ul class="list-indent2"><li>indent 2 times line 1</li></ul></ul><br/><ul class="list-indent1"><ul class="list-indent2"><li>indent 2 times line 2</li></ul></ul></body></html>';
importrequest(htmlWithIndents, importurl, 'html'); importrequest(htmlWithIndents, importurl, 'html');
helper.waitFor(() => expect(getinnertext()).to.be('\ helper.waitFor(() => expect(getinnertext()).to.be(
<ul class="list-indent1"><li><span class="">indent line 1</span></li></ul>\n\ '<ul class="list-indent1"><li><span class="">indent line 1</span></li></ul>\n' +
<br>\n\ '<br>\n' +
<ul class="list-indent1"><li><span class="">indent 1 line 2</span></li></ul>\n\ '<ul class="list-indent1"><li><span class="">indent 1 line 2</span></li></ul>\n' +
<ul class="list-indent2"><li><span class="">indent 2 times line 1</span></li></ul>\n\ '<ul class="list-indent2"><li><span class="">indent 2 times line 1</span></li></ul>\n' +
<br>\n\ '<br>\n' +
<ul class="list-indent2"><li><span class="">indent 2 times line 2</span></li></ul>\n\ '<ul class="list-indent2"><li><span class="">indent 2 times line 2</span></li></ul>\n' +
<br>\n')); '<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href); const results = exportfunc(helper.padChrome$.window.location.href);
/* eslint-disable-next-line max-len */
expect(results[0][1]).to.be('<ul class="indent"><li>indent line 1</li></ul><br><ul class="indent"><li>indent 1 line 2</li><ul class="indent"><li>indent 2 times line 1</li></ul></ul><br><ul><ul class="indent"><li>indent 2 times line 2</li></ul></ul><br>'); expect(results[0][1]).to.be('<ul class="indent"><li>indent line 1</li></ul><br><ul class="indent"><li>indent 1 line 2</li><ul class="indent"><li>indent 2 times line 1</li></ul></ul><br><ul><ul class="indent"><li>indent 2 times line 2</li></ul></ul><br>');
/* 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'); 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(); 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`; const importurl = `${helper.padChrome$.window.location.href}/import`;
/* eslint-disable-next-line max-len */
const htmlWithIndents = '<html><body><ul class="list-indent1"><li>indent line 1</li></ul><br/><ul class="list-indent1"><li>indent line 2</li><ul class="list-indent2"><li>indent2 line 1</li></ul></ul><br/><ul class="list-indent1"><ul class="list-indent2"><ul class="list-indent3"><ul class="list-indent4"><li><span class="b s i u"><b><i><s><u>indent4 line 2 bisu</u></s></i></b></span></li><li><span class="b s "><b><s>indent4 line 2 bs</s></b></span></li><li><span class="u"><u>indent4 line 2 u</u></span><span class="u i s"><i><s><u>uis</u></s></i></span></li><ul class="list-indent5"><ul class="list-indent6"><ul class="list-indent7"><ul class="list-indent8"><li><span class="">foo</span></li><li><span class="b s"><b><s>foobar bs</b></s></span></li></ul></ul></ul></ul><ul class="list-indent5"><li>foobar</li></ul></ul></ul></ul></body></html>'; const htmlWithIndents = '<html><body><ul class="list-indent1"><li>indent line 1</li></ul><br/><ul class="list-indent1"><li>indent line 2</li><ul class="list-indent2"><li>indent2 line 1</li></ul></ul><br/><ul class="list-indent1"><ul class="list-indent2"><ul class="list-indent3"><ul class="list-indent4"><li><span class="b s i u"><b><i><s><u>indent4 line 2 bisu</u></s></i></b></span></li><li><span class="b s "><b><s>indent4 line 2 bs</s></b></span></li><li><span class="u"><u>indent4 line 2 u</u></span><span class="u i s"><i><s><u>uis</u></s></i></span></li><ul class="list-indent5"><ul class="list-indent6"><ul class="list-indent7"><ul class="list-indent8"><li><span class="">foo</span></li><li><span class="b s"><b><s>foobar bs</b></s></span></li></ul></ul></ul></ul><ul class="list-indent5"><li>foobar</li></ul></ul></ul></ul></body></html>';
importrequest(htmlWithIndents, importurl, 'html'); importrequest(htmlWithIndents, importurl, 'html');
helper.waitFor(() => expect(getinnertext()).to.be('\ helper.waitFor(() => expect(getinnertext()).to.be(
<ul class="list-indent1"><li><span class="">indent line 1</span></li></ul>\n\<br>\n\ '<ul class="list-indent1"><li><span class="">indent line 1</span></li></ul>\n<br>\n' +
<ul class="list-indent1"><li><span class="">indent line 2</span></li></ul>\n\ '<ul class="list-indent1"><li><span class="">indent line 2</span></li></ul>\n' +
<ul class="list-indent2"><li><span class="">indent2 line 1</span></li></ul>\n<br>\n\ '<ul class="list-indent2"><li><span class="">indent2 line 1</span></li></ul>\n<br>\n' +
<ul class="list-indent4"><li><span class="b i s u"><b><i><s><u>indent4 line 2 bisu</u></s></i></b></span></li></ul>\n\ '<ul class="list-indent4"><li><span class="b i s u"><b><i><s><u>indent4 ' +
<ul class="list-indent4"><li><span class="b s"><b><s>indent4 line 2 bs</s></b></span></li></ul>\n\ 'line 2 bisu</u></s></i></b></span></li></ul>\n' +
<ul class="list-indent4"><li><span class="u"><u>indent4 line 2 u</u></span><span class="i s u"><i><s><u>uis</u></s></i></span></li></ul>\n\ '<ul class="list-indent4"><li><span class="b s"><b><s>' +
<ul class="list-indent8"><li><span class="">foo</span></li></ul>\n\ 'indent4 line 2 bs</s></b></span></li></ul>\n' +
<ul class="list-indent8"><li><span class="b s"><b><s>foobar bs</s></b></span></li></ul>\n\ '<ul class="list-indent4"><li><span class="u"><u>indent4 line 2 u</u>' +
<ul class="list-indent5"><li><span class="">foobar</span></li></ul>\n\ '</span><span class="i s u"><i><s><u>uis</u></s></i></span></li></ul>\n' +
<br>\n')); '<ul class="list-indent8"><li><span class="">foo</span></li></ul>\n' +
'<ul class="list-indent8"><li><span class="b s"><b><s>foobar bs</s></b>' +
'</span></li></ul>\n' +
'<ul class="list-indent5"><li><span class="">foobar</span></li></ul>\n' +
'<br>\n'));
const results = exportfunc(helper.padChrome$.window.location.href); const results = exportfunc(helper.padChrome$.window.location.href);
/* eslint-disable-next-line max-len */
expect(results[0][1]).to.be('<ul class="indent"><li>indent line 1</li></ul><br><ul class="indent"><li>indent line 2</li><ul class="indent"><li>indent2 line 1</li></ul></ul><br><ul><ul><ul><ul class="indent"><li><strong><em><s><u>indent4 line 2 bisu</u></s></em></strong></li><li><strong><s>indent4 line 2 bs</s></strong></li><li><u>indent4 line 2 u<em><s>uis</s></em></u></li><ul><ul><ul><ul class="indent"><li>foo</li><li><strong><s>foobar bs</s></strong></li></ul></ul></ul><li>foobar</li></ul></ul></ul></ul></ul><br>'); expect(results[0][1]).to.be('<ul class="indent"><li>indent line 1</li></ul><br><ul class="indent"><li>indent line 2</li><ul class="indent"><li>indent2 line 1</li></ul></ul><br><ul><ul><ul><ul class="indent"><li><strong><em><s><u>indent4 line 2 bisu</u></s></em></strong></li><li><strong><s>indent4 line 2 bs</s></strong></li><li><u>indent4 line 2 u<em><s>uis</s></em></u></li><ul><ul><ul><ul class="indent"><li>foo</li><li><strong><s>foobar bs</s></strong></li></ul></ul></ul><li>foobar</li></ul></ul></ul></ul></ul><br>');
/* 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'); 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(); done();
}); });

View file

@ -17,7 +17,7 @@ describe('indentation button', function () {
// select this text element // select this text element
$firstTextElement.sendkeys('{selectall}'); $firstTextElement.sendkeys('{selectall}');
const e = inner$.Event(helper.evtType); const e = new inner$.Event(helper.evtType);
e.keyCode = 9; // tab :| e.keyCode = 9; // tab :|
inner$('#innerdocbody').trigger(e); 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); this.timeout(1200);
const inner$ = helper.padInner$; 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); this.timeout(1200);
const inner$ = helper.padInner$; 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); this.timeout(1200);
const inner$ = helper.padInner$; const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$; const chrome$ = helper.padChrome$;
@ -189,7 +192,6 @@ describe('indentation button', function () {
var $indentButton = testHelper.$getPadChrome().find(".buttonicon-indent"); var $indentButton = testHelper.$getPadChrome().find(".buttonicon-indent");
$indentButton.click(); $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(); var newFirstTextElement = $inner.find("div").first();
// is there a list-indent class element now? // is there a list-indent class element now?
@ -227,7 +229,6 @@ describe('indentation button', function () {
$outdentButton.click(); $outdentButton.click();
$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(); var newFirstTextElement = $inner.find("div").first();
// is there a list-indent class element now? // 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 //get the second text element out of the inner iframe
setTimeout(function(){ // THIS IS REALLY BAD 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? // is there a list-indent class element now?
var firstChild = secondTextElement.children(":first"); var firstChild = secondTextElement.children(":first");
@ -289,7 +292,10 @@ describe('indentation button', function () {
expect(isLI).to.be(true); expect(isLI).to.be(true);
//get the first text element out of the inner iframe //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? // is there a list-indent class element now?
var firstChild = thirdTextElement.children(":first"); var firstChild = thirdTextElement.children(":first");
@ -309,7 +315,7 @@ describe('indentation button', function () {
const pressEnter = () => { const pressEnter = () => {
const inner$ = helper.padInner$; const inner$ = helper.padInner$;
const e = inner$.Event(helper.evtType); const e = new inner$.Event(helper.evtType);
e.keyCode = 13; // enter :| e.keyCode = 13; // enter :|
inner$('#innerdocbody').trigger(e); inner$('#innerdocbody').trigger(e);
}; };

View file

@ -46,7 +46,7 @@ describe('italic some text', function () {
// select this text element // select this text element
$firstTextElement.sendkeys('{selectall}'); $firstTextElement.sendkeys('{selectall}');
const e = inner$.Event(helper.evtType); const e = new inner$.Event(helper.evtType);
e.ctrlKey = true; // Control key e.ctrlKey = true; // Control key
e.which = 105; // i e.which = 105; // i
inner$('#innerdocbody').trigger(e); inner$('#innerdocbody').trigger(e);

View file

@ -15,8 +15,8 @@ describe('author of pad edition', function () {
setTimeout(() => { setTimeout(() => {
// Expire cookie, so author is changed after reloading the pad. // 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 // 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 =
helper.padChrome$.document.cookie = cookieVal; 'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
helper.newPad(done, padId); helper.newPad(done, padId);
}, 1000); }, 1000);
@ -25,7 +25,12 @@ describe('author of pad edition', function () {
this.timeout(60000); 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 inner$ = helper.padInner$;
const chrome$ = helper.padChrome$; const chrome$ = helper.padChrome$;

View file

@ -39,7 +39,8 @@ describe('assign ordered list', function () {
it('does not insert unordered list', function (done) { it('does not insert unordered list', function (done) {
this.timeout(3000); 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'); expect().fail(() => 'Unordered list inserted, should ignore shortcut');
}).fail(() => { }).fail(() => {
done(); done();
@ -69,7 +70,8 @@ describe('assign ordered list', function () {
it('does not insert unordered list', function (done) { it('does not insert unordered list', function (done) {
this.timeout(3000); 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'); expect().fail(() => 'Unordered list inserted, should ignore shortcut');
}).fail(() => { }).fail(() => {
done(); 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 inner$ = helper.padInner$;
const chrome$ = helper.padChrome$; 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 inner$ = helper.padInner$;
const e = inner$.Event(helper.evtType); const e = new inner$.Event(helper.evtType);
e.ctrlKey = true; e.ctrlKey = true;
e.shiftKey = true; e.shiftKey = true;
e.which = shortcutChar.toString().charCodeAt(0); e.which = shortcutChar.toString().charCodeAt(0);
inner$('#innerdocbody').trigger(e); inner$('#innerdocbody').trigger(e);
}; };
const makeSureShortcutIsDisabled = function (shortcut) { const makeSureShortcutIsDisabled = (shortcut) => {
helper.padChrome$.window.clientVars.padShortcutEnabled[shortcut] = false; helper.padChrome$.window.clientVars.padShortcutEnabled[shortcut] = false;
}; };
const makeSureShortcutIsEnabled = function (shortcut) { const makeSureShortcutIsEnabled = (shortcut) => {
helper.padChrome$.window.clientVars.padShortcutEnabled[shortcut] = true; 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'); const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist');
$insertorderedlistButton.click(); $insertorderedlistButton.click();
const e = inner$.Event(helper.evtType); const e = new inner$.Event(helper.evtType);
e.keyCode = 9; // tab e.keyCode = 9; // tab
inner$('#innerdocbody').trigger(e); 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 // create a new pad before each test run
beforeEach(function (cb) { beforeEach(function (cb) {
helper.newPad(cb); helper.newPad(cb);

View file

@ -100,17 +100,17 @@ describe('Pad modal', function () {
}); });
}); });
const clickOnPadInner = function () { const clickOnPadInner = () => {
const $editor = helper.padInner$('#innerdocbody'); const $editor = helper.padInner$('#innerdocbody');
$editor.click(); $editor.click();
}; };
const clickOnPadOuter = function () { const clickOnPadOuter = () => {
const $lineNumbersColumn = helper.padOuter$('#sidedivinner'); const $lineNumbersColumn = helper.padOuter$('#sidedivinner');
$lineNumbersColumn.click(); $lineNumbersColumn.click();
}; };
const openSettingsAndWaitForModalToBeVisible = function (done) { const openSettingsAndWaitForModalToBeVisible = (done) => {
helper.padChrome$('.buttonicon-settings').click(); helper.padChrome$('.buttonicon-settings').click();
// wait for modal to be displayed // wait for modal to be displayed
@ -118,7 +118,7 @@ describe('Pad modal', function () {
helper.waitFor(() => isModalOpened(modalSelector), 10000).done(done); helper.waitFor(() => isModalOpened(modalSelector), 10000).done(done);
}; };
const isEditorDisabled = function () { const isEditorDisabled = () => {
const editorDocument = helper.padOuter$("iframe[name='ace_inner']").get(0).contentDocument; const editorDocument = helper.padOuter$("iframe[name='ace_inner']").get(0).contentDocument;
const editorBody = editorDocument.getElementById('innerdocbody'); const editorBody = editorDocument.getElementById('innerdocbody');
@ -128,7 +128,7 @@ describe('Pad modal', function () {
return editorIsDisabled; return editorIsDisabled;
}; };
const isModalOpened = function (modalSelector) { const isModalOpened = (modalSelector) => {
const $modal = helper.padChrome$(modalSelector); const $modal = helper.padChrome$(modalSelector);
return $modal.hasClass('popup-show'); return $modal.hasClass('popup-show');

View file

@ -3,14 +3,17 @@
// Test for https://github.com/ether/etherpad-lite/issues/1763 // Test for https://github.com/ether/etherpad-lite/issues/1763
// This test fails in Opera, IE and Safari // 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 // 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 // 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. // 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. // 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. // visible to the user.
// Adapted from John McLear's original test case. // Adapted from John McLear's original test case.
@ -22,16 +25,18 @@ xdescribe('Responsiveness of Editor', function () {
this.timeout(6000); this.timeout(6000);
}); });
// JM commented out on 8th Sep 2020 for a release, after release this needs uncommenting // 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 // And the test needs to be fixed to work in Firefox 52 on Windows 7.
// The errors show this.timeout... then crash the browser but I am sure something is actually causing the stack trace and // 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. // 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) { it('Fast response to keypress in pad with large amount of contents', function (done) {
// skip on Windows Firefox 52.0 // 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(); this.skip();
} }
const inner$ = helper.padInner$; const inner$ = helper.padInner$;
const chrome$ = helper.padChrome$;
const chars = '0000000000'; // row of placeholder chars const chars = '0000000000'; // row of placeholder chars
const amount = 200000; // number of blocks of chars we will insert const amount = 200000; // number of blocks of chars we will insert
const length = (amount * (chars.length) + 1); // include a counter for each space 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 // get keys to send
const keyMultiplier = 10; // multiplier * 10 == total number of key events const keyMultiplier = 10; // multiplier * 10 == total number of key events
let keysToSend = ''; let keysToSend = '';
for (var i = 0; i <= keyMultiplier; i++) { for (let i = 0; i <= keyMultiplier; i++) {
keysToSend += chars; keysToSend += chars;
} }
@ -49,23 +54,23 @@ xdescribe('Responsiveness of Editor', function () {
textElement.sendkeys('{selectall}'); // select all textElement.sendkeys('{selectall}'); // select all
textElement.sendkeys('{del}'); // clear the pad text 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 text = `${text + chars} `; // add the chars and space to the text contents
} }
inner$('div').first().text(text); // Put the text contents into the pad inner$('div').first().text(text); // Put the text contents into the pad
helper.waitFor(() => // Wait for the new contents to be on the pad // Wait for the new contents to be on the pad
inner$('div').text().length > length helper.waitFor(() => inner$('div').text().length > length).done(() => {
).done(() => { // has the text changed?
expect(inner$('div').text().length).to.be.greaterThan(length); // has the text changed? expect(inner$('div').text().length).to.be.greaterThan(length);
const start = Date.now(); // get the start time const start = Date.now(); // get the start time
// send some new text to the screen (ensure all 3 key events are sent) // send some new text to the screen (ensure all 3 key events are sent)
const el = inner$('div').first(); const el = inner$('div').first();
for (let i = 0; i < keysToSend.length; ++i) { for (let i = 0; i < keysToSend.length; ++i) {
var x = keysToSend.charCodeAt(i); const x = keysToSend.charCodeAt(i);
['keyup', 'keypress', 'keydown'].forEach((type) => { ['keyup', 'keypress', 'keydown'].forEach((type) => {
const e = $.Event(type); const e = new $.Event(type);
e.keyCode = x; e.keyCode = x;
el.trigger(e); el.trigger(e);
}); });

View file

@ -91,7 +91,7 @@ describe('select formatting buttons when selection has style applied', function
// select this text element // select this text element
$firstTextElement.sendkeys('{selectall}'); $firstTextElement.sendkeys('{selectall}');
const e = inner$.Event(helper.evtType); const e = new inner$.Event(helper.evtType);
e.ctrlKey = true; // Control key e.ctrlKey = true; // Control key
e.which = key.charCodeAt(0); // I, U, B, 5 e.which = key.charCodeAt(0); // I, U, B, 5
inner$('#innerdocbody').trigger(e); inner$('#innerdocbody').trigger(e);

View file

@ -22,8 +22,6 @@ describe('strikethrough button', function () {
const $strikethroughButton = chrome$('.buttonicon-strikethrough'); const $strikethroughButton = chrome$('.buttonicon-strikethrough');
$strikethroughButton.click(); $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(); const $newFirstTextElement = inner$('div').first();
// is there a <i> element now? // is there a <i> element now?

View file

@ -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 // get the first text element inside the editable space
const $firstTextElement = inner$('div span').first(); const $firstTextElement = inner$('div span').first();
const originalValue = $firstTextElement.text(); // get the original value const originalValue = $firstTextElement.text(); // get the original value
const newValue = `Testing${originalValue}`;
$firstTextElement.sendkeys('Testing'); // send line 1 to the pad $firstTextElement.sendkeys('Testing'); // send line 1 to the pad
const modifiedValue = $firstTextElement.text(); // get the modified value const modifiedValue = $firstTextElement.text(); // get the modified value

View file

@ -9,8 +9,10 @@ describe('timeslider', function () {
/** /**
* @todo test authorsList * @todo test authorsList
*/ */
it('Shows a correctly formatted date and time', async function () { it('Shows a correctly formatted date and time', async function () {
this.timeout(12000); this.timeout(12000);
// make some changes to produce 3 revisions // make some changes to produce 3 revisions
const revs = 3; const revs = 3;

View file

@ -26,8 +26,8 @@ describe('timeslider', function () {
setTimeout(() => { setTimeout(() => {
// go to timeslider // go to timeslider
$('#iframe-container iframe') $('#iframe-container iframe').attr('src',
.attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider`); `${$('#iframe-container iframe').attr('src')}/timeslider`);
setTimeout(() => { setTimeout(() => {
const timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; const timeslider$ = $('#iframe-container iframe')[0].contentWindow.$;
@ -84,8 +84,8 @@ describe('timeslider', function () {
setTimeout(() => { setTimeout(() => {
// go to timeslider // go to timeslider
$('#iframe-container iframe') $('#iframe-container iframe').attr('src',
.attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider`); `${$('#iframe-container iframe').attr('src')}/timeslider`);
setTimeout(() => { setTimeout(() => {
const timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; const timeslider$ = $('#iframe-container iframe')[0].contentWindow.$;
@ -102,15 +102,16 @@ describe('timeslider', function () {
helper.waitFor( helper.waitFor(
() => $('#iframe-container iframe')[0].contentWindow.location.hash !== oldUrl, 6000) () => $('#iframe-container iframe')[0].contentWindow.location.hash !== oldUrl, 6000)
.always(() => { .always(() => {
expect($('#iframe-container iframe')[0].contentWindow.location.hash) expect(
.not.to.eql(oldUrl); $('#iframe-container iframe')[0].contentWindow.location.hash
).not.to.eql(oldUrl);
done(); done();
}); });
}, 6000); }, 6000);
}, revs * timePerRev); }, revs * timePerRev);
}); });
it('jumps to a revision given in the url', function (done) { it('jumps to a revision given in the url', function (done) {
this.timeout(6000); this.timeout(40000);
const inner$ = helper.padInner$; const inner$ = helper.padInner$;
// wait for the text to be loaded // wait for the text to be loaded
@ -132,15 +133,16 @@ describe('timeslider', function () {
return lenOkay && colorOkay; return lenOkay && colorOkay;
}, 10000).always(() => { }, 10000).always(() => {
// go to timeslider with a specific revision set // 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 // wait for the timeslider to be loaded
helper.waitFor(() => { helper.waitFor(() => {
try { try {
timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; timeslider$ = $('#iframe-container iframe')[0].contentWindow.$;
} catch (e) { } catch (e) {
// silently fart. Deadly. // Empty catch block <3
} }
if (timeslider$) { if (timeslider$) {
return timeslider$('#innerdocbody').text().length === oldLength; return timeslider$('#innerdocbody').text().length === oldLength;
@ -161,8 +163,8 @@ describe('timeslider', function () {
setTimeout(() => { setTimeout(() => {
// go to timeslider // go to timeslider
$('#iframe-container iframe') $('#iframe-container iframe').attr('src',
.attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider#0`); `${$('#iframe-container iframe').attr('src')}/timeslider#0`);
let timeslider$; let timeslider$;
let exportLink; let exportLink;
@ -170,7 +172,7 @@ describe('timeslider', function () {
try { try {
timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; timeslider$ = $('#iframe-container iframe')[0].contentWindow.$;
} catch (e) { } catch (e) {
// silently give up on life. // Empty catch block <3
} }
if (!timeslider$) return false; if (!timeslider$) return false;
exportLink = timeslider$('#exportplaina').attr('href'); exportLink = timeslider$('#exportplaina').attr('href');

View file

@ -43,7 +43,7 @@ describe('undo button', function () {
const modifiedValue = $firstTextElement.text(); // get the modified value const modifiedValue = $firstTextElement.text(); // get the modified value
expect(modifiedValue).not.to.be(originalValue); // expect the value to change 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.ctrlKey = true; // Control key
e.which = 90; // z e.which = 90; // z
inner$('#innerdocbody').trigger(e); inner$('#innerdocbody').trigger(e);

View file

@ -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 // create a new pad before each test run
beforeEach(function (cb) { beforeEach(function (cb) {
helper.newPad(cb); helper.newPad(cb);

View file

@ -1,17 +1,19 @@
var srcFolder = '../../../src/node_modules/'; 'use strict';
var wd = require(`${srcFolder}wd`);
var async = require(`${srcFolder}async`);
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', host: 'ondemand.saucelabs.com',
port: 80, port: 80,
username: process.env.SAUCE_USER, username: process.env.SAUCE_USER,
accessKey: process.env.SAUCE_ACCESS_KEY, accessKey: process.env.SAUCE_ACCESS_KEY,
}; };
var allTestsPassed = true; let allTestsPassed = true;
// overwrite the default exit code // 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 // and the script would silently exit with error code 0
process.exitCode = 2; process.exitCode = 2;
process.on('exit', (code) => { process.on('exit', (code) => {
@ -20,13 +22,18 @@ process.on('exit', (code) => {
} }
}); });
var sauceTestWorker = async.queue((testSettings, callback) => { const sauceTestWorker = async.queue((testSettings, callback) => {
const browser = wd.promiseChainRemote(config.host, config.port, config.username, config.accessKey); const browser = wd.promiseChainRemote(
const name = `${process.env.GIT_HASH} - ${testSettings.browserName} ${testSettings.version}, ${testSettings.platform}`; config.host, config.port, config.username, config.accessKey);
const name =
`${process.env.GIT_HASH} - ${testSettings.browserName} ` +
`${testSettings.version}, ${testSettings.platform}`;
testSettings.name = name; testSettings.name = name;
testSettings.public = true; testSettings.public = true;
testSettings.build = process.env.GIT_HASH; 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; testSettings.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER;
browser.init(testSettings).get('http://localhost:9001/tests/frontend/', () => { 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}`); console.log(`Remote sauce test '${name}' started! ${url}`);
// tear down the test excecution // tear down the test excecution
const stopSauce = function (success, timesup) { const stopSauce = (success, timesup) => {
clearInterval(getStatusInterval); clearInterval(getStatusInterval);
clearTimeout(timeout); clearTimeout(timeout);
@ -43,12 +50,15 @@ var sauceTestWorker = async.queue((testSettings, callback) => {
allTestsPassed = false; 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. // five seconds may not be available here. It's an error anyway, so don't care about it.
printLog(logIndex); printLog(logIndex);
if (timesup) { 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}`); 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 * 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 * It's necessary because if travis kills the saucelabs session due to inactivity,
* @todo this should be configured in testSettings, see https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-Timeouts * 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); stopSauce(false, true);
}, 870000); // travis timeout is 15 minutes, set this to a slightly lower value }, 870000); // travis timeout is 15 minutes, set this to a slightly lower value
let knownConsoleText = ''; let knownConsoleText = '';
// how many characters of the log have been sent to travis // how many characters of the log have been sent to travis
let logIndex = 0; let logIndex = 0;
var getStatusInterval = setInterval(() => { const getStatusInterval = setInterval(() => {
browser.eval("$('#console').text()", (err, consoleText) => { browser.eval("$('#console').text()", (err, consoleText) => {
if (!consoleText || err) { if (!consoleText || err) {
return; return;
@ -76,9 +88,10 @@ var sauceTestWorker = async.queue((testSettings, callback) => {
knownConsoleText = consoleText; knownConsoleText = consoleText;
if (knownConsoleText.indexOf('FINISHED') > 0) { 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 // finished without failures
if (match[2] && match[2] == '0') { if (match[2] && match[2] === '0') {
stopSauce(true); stopSauce(true);
// finished but some tests did not return or some tests failed // 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 * @param {number} index offset from where to start
*/ */
function printLog(index) { const printLog = (index) => {
let testResult = knownConsoleText.substring(index).replace(/\[red\]/g, '\x1B[31m').replace(/\[yellow\]/g, '\x1B[33m') 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'); .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); console.log(testResult);
} };
}); });
}, 6); // run 6 tests in parrallel }, 6); // run 6 tests in parrallel

View file

@ -1,8 +1,12 @@
'use strict';
let etherpad;
try { try {
var etherpad = require('../../src/node_modules/etherpad-cli-client'); etherpad = require('ep_etherpad-lite/node_modules/etherpad-cli-client');
// ugly // ugly
} catch { } 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]); const pad = etherpad.connect(process.argv[2]);
pad.on('connected', () => { pad.on('connected', () => {
@ -18,7 +22,7 @@ pad.on('connected', () => {
}); });
// in case of disconnect exit code 1 // in case of disconnect exit code 1
pad.on('message', (message) => { pad.on('message', (message) => {
if (message.disconnect == 'rateLimited') { if (message.disconnect === 'rateLimited') {
process.exit(1); process.exit(1);
} }
}); });