From 0cc8405e9c8e2c7acfe5607c46907287c0a52006 Mon Sep 17 00:00:00 2001 From: John McLear Date: Tue, 19 Jan 2021 16:37:12 +0000 Subject: [PATCH 01/31] Bump minimum required Node.js version to 10.17.0 This makes it possible to use fs.promises. --- CHANGELOG.md | 4 ++++ README.md | 4 ++-- bin/plugins/checkPlugin.js | 2 +- doc/plugins.md | 2 +- package.json | 2 +- src/node/server.js | 4 ++-- src/package.json | 2 +- 7 files changed, 12 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b39ce09ec..42c2692b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ # Changes for the next release + +### Compatibility changes +* Node.js 10.17.0 or newer is now required. + ### Notable new features * Database performance is significantly improved. diff --git a/README.md b/README.md index 10dbd36cd..5a66ddd78 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Etherpad is a real-time collaborative editor [scalable to thousands of simultane # Installation ## Requirements -- `nodejs` >= **10.13.0**. +- `nodejs` >= **10.17.0**. ## GNU/Linux and other UNIX-like systems @@ -25,7 +25,7 @@ git clone --branch master https://github.com/ether/etherpad-lite.git && cd ether ``` ### Manual install -You'll need git and [node.js](https://nodejs.org) installed (minimum required Node version: **10.13.0**). +You'll need git and [node.js](https://nodejs.org) installed (minimum required Node version: **10.17.0**). **As any user (we recommend creating a separate user called etherpad):** diff --git a/bin/plugins/checkPlugin.js b/bin/plugins/checkPlugin.js index cbe146aa1..0b736af80 100755 --- a/bin/plugins/checkPlugin.js +++ b/bin/plugins/checkPlugin.js @@ -263,7 +263,7 @@ fs.readdir(pluginPath, (err, rootFiles) => { console.warn('No engines or node engine in package.json'); if (autoFix) { const engines = { - node: '>=10.13.0', + node: '^10.17.0 || >=11.14.0', }; parsedPackageJSON.engines = engines; writePackageJson(parsedPackageJSON); diff --git a/doc/plugins.md b/doc/plugins.md index 2062378bb..d8239c68a 100644 --- a/doc/plugins.md +++ b/doc/plugins.md @@ -225,7 +225,7 @@ publish your plugin. "author": "USERNAME (REAL NAME) ", "contributors": [], "dependencies": {"MODULE": "0.3.20"}, - "engines": { "node": ">= 10.13.0"} + "engines": { "node": "^10.17.0 || >=11.14.0"} } ``` diff --git a/package.json b/package.json index 41e472831..03cb677ba 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,6 @@ "lint": "eslint ." }, "engines": { - "node": ">=10.13.0" + "node": "^10.17.0 || >=11.14.0" } } diff --git a/src/node/server.js b/src/node/server.js index bbe4c7aeb..a0d9e2adc 100755 --- a/src/node/server.js +++ b/src/node/server.js @@ -36,8 +36,8 @@ const wtfnode = require('wtfnode'); * any modules that require newer versions of NodeJS */ const NodeVersion = require('./utils/NodeVersion'); -NodeVersion.enforceMinNodeVersion('10.13.0'); -NodeVersion.checkDeprecationStatus('10.13.0', '1.8.3'); +NodeVersion.enforceMinNodeVersion('10.17.0'); +NodeVersion.checkDeprecationStatus('10.17.0', '1.8.8'); const UpdateCheck = require('./utils/UpdateCheck'); const db = require('./db/DB'); diff --git a/src/package.json b/src/package.json index 816f5b587..8399c19bd 100644 --- a/src/package.json +++ b/src/package.json @@ -139,7 +139,7 @@ "root": true }, "engines": { - "node": ">=10.13.0", + "node": "^10.17.0 || >=11.14.0", "npm": ">=5.5.1" }, "repository": { From b3dda3b11c23f36942d283c0974a3664268f6242 Mon Sep 17 00:00:00 2001 From: John McLear Date: Tue, 19 Jan 2021 16:37:12 +0000 Subject: [PATCH 02/31] lint: src/static/js/pluginfw/*.js --- src/static/js/pluginfw/hooks.js | 56 ++++++++++----------- src/static/js/pluginfw/installer.js | 72 ++++++++++++++------------- src/static/js/pluginfw/plugin_defs.js | 2 + src/static/js/pluginfw/plugins.js | 51 ++++++++----------- src/static/js/pluginfw/shared.js | 34 +++++++------ 5 files changed, 109 insertions(+), 106 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 6243a9305..e61079eb6 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -1,4 +1,4 @@ -/* global exports, require */ +'use strict'; const _ = require('underscore'); const pluginDefs = require('./plugin_defs'); @@ -15,30 +15,28 @@ exports.deprecationNotices = {}; const deprecationWarned = {}; -function checkDeprecation(hook) { +const checkDeprecation = (hook) => { const notice = exports.deprecationNotices[hook.hook_name]; if (notice == null) return; if (deprecationWarned[hook.hook_fn_name]) return; console.warn(`${hook.hook_name} hook used by the ${hook.part.plugin} plugin ` + `(${hook.hook_fn_name}) is deprecated: ${notice}`); deprecationWarned[hook.hook_fn_name] = true; -} +}; exports.bubbleExceptions = true; -const hookCallWrapper = function (hook, hook_name, args, cb) { - if (cb === undefined) cb = function (x) { return x; }; +const hookCallWrapper = (hook, hook_name, args, cb) => { + if (cb === undefined) cb = (x) => x; checkDeprecation(hook); // Normalize output to list for both sync and async cases - const normalize = function (x) { + const normalize = (x) => { if (x === undefined) return []; return x; }; - const normalizedhook = function () { - return normalize(hook.hook_fn(hook_name, args, (x) => cb(normalize(x)))); - }; + const normalizedhook = () => normalize(hook.hook_fn(hook_name, args, (x) => cb(normalize(x)))); if (exports.bubbleExceptions) { return normalizedhook(); @@ -51,7 +49,7 @@ const hookCallWrapper = function (hook, hook_name, args, cb) { } }; -exports.syncMapFirst = function (lst, fn) { +exports.syncMapFirst = (lst, fn) => { let i; let result; for (i = 0; i < lst.length; i++) { @@ -61,11 +59,11 @@ exports.syncMapFirst = function (lst, fn) { return []; }; -exports.mapFirst = function (lst, fn, cb, predicate) { +exports.mapFirst = (lst, fn, cb, predicate) => { if (predicate == null) predicate = (x) => (x != null && x.length > 0); let i = 0; - var next = function () { + const next = () => { if (i >= lst.length) return cb(null, []); fn(lst[i++], (err, result) => { if (err) return cb(err); @@ -104,7 +102,7 @@ exports.mapFirst = function (lst, fn, cb, predicate) { // // See the tests in tests/backend/specs/hooks.js for examples of supported and prohibited behaviors. // -function callHookFnSync(hook, context) { +const callHookFnSync = (hook, context) => { checkDeprecation(hook); // This var is used to keep track of whether the hook function already settled. @@ -190,7 +188,7 @@ function callHookFnSync(hook, context) { settle(null, val, 'returned value'); return outcome.val; -} +}; // Invokes all registered hook functions synchronously. // @@ -203,7 +201,7 @@ function callHookFnSync(hook, context) { // 1. Collect all values returned by the hook functions into an array. // 2. Convert each `undefined` entry into `[]`. // 3. Flatten one level. -exports.callAll = function (hookName, context) { +exports.callAll = (hookName, context) => { if (context == null) context = {}; const hooks = pluginDefs.hooks[hookName] || []; return _.flatten(hooks.map((hook) => { @@ -248,7 +246,7 @@ exports.callAll = function (hookName, context) { // // See the tests in tests/backend/specs/hooks.js for examples of supported and prohibited behaviors. // -async function callHookFnAsync(hook, context) { +const callHookFnAsync = async (hook, context) => { checkDeprecation(hook); return await new Promise((resolve, reject) => { // This var is used to keep track of whether the hook function already settled. @@ -326,7 +324,7 @@ async function callHookFnAsync(hook, context) { (val) => settle(null, val, 'returned value'), (err) => settle(err, null, 'Promise rejection')); }); -} +}; // Invokes all registered hook functions asynchronously. // @@ -350,20 +348,22 @@ exports.aCallAll = async (hookName, context, cb) => { const hooks = pluginDefs.hooks[hookName] || []; let resultsPromise = Promise.all(hooks.map((hook) => callHookFnAsync(hook, context) // `undefined` (but not `null`!) is treated the same as []. - .then((result) => (result === undefined) ? [] : result))).then((results) => _.flatten(results, 1)); + .then((result) => (result === undefined) ? [] : result))) + .then((results) => _.flatten(results, 1)); if (cb != null) resultsPromise = resultsPromise.then((val) => cb(null, val), cb); return await resultsPromise; }; -exports.callFirst = function (hook_name, args) { +exports.callFirst = (hook_name, args) => { if (!args) args = {}; if (pluginDefs.hooks[hook_name] === undefined) return []; - return exports.syncMapFirst(pluginDefs.hooks[hook_name], (hook) => hookCallWrapper(hook, hook_name, args)); + return exports.syncMapFirst(pluginDefs.hooks[hook_name], + (hook) => hookCallWrapper(hook, hook_name, args)); }; -function aCallFirst(hook_name, args, cb, predicate) { +const aCallFirst = (hook_name, args, cb, predicate) => { if (!args) args = {}; - if (!cb) cb = function () {}; + if (!cb) cb = () => {}; if (pluginDefs.hooks[hook_name] === undefined) return cb(null, []); exports.mapFirst( pluginDefs.hooks[hook_name], @@ -373,10 +373,10 @@ function aCallFirst(hook_name, args, cb, predicate) { cb, predicate ); -} +}; /* return a Promise if cb is not supplied */ -exports.aCallFirst = function (hook_name, args, cb, predicate) { +exports.aCallFirst = (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); @@ -386,10 +386,10 @@ exports.aCallFirst = function (hook_name, args, cb, predicate) { } }; -exports.callAllStr = function (hook_name, args, sep, pre, post) { - if (sep == undefined) sep = ''; - if (pre == undefined) pre = ''; - if (post == undefined) post = ''; +exports.callAllStr = (hook_name, args, sep, pre, post) => { + if (sep === undefined) sep = ''; + if (pre === undefined) pre = ''; + if (post === undefined) post = ''; const newCallhooks = []; const callhooks = exports.callAll(hook_name, args); for (let i = 0, ii = callhooks.length; i < ii; i++) { diff --git a/src/static/js/pluginfw/installer.js b/src/static/js/pluginfw/installer.js index 7d29b91b1..ae5c06e10 100644 --- a/src/static/js/pluginfw/installer.js +++ b/src/static/js/pluginfw/installer.js @@ -1,6 +1,8 @@ +'use strict'; + const log4js = require('log4js'); -const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugins'); -const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); +const plugins = require('./plugins'); +const hooks = require('./hooks'); const npm = require('npm'); const request = require('request'); const util = require('util'); @@ -13,22 +15,22 @@ const loadNpm = async () => { npm.on('log', log4js.getLogger('npm').log); }; +const onAllTasksFinished = () => { + hooks.aCallAll('restartServer', {}, () => {}); +}; + let tasks = 0; function wrapTaskCb(cb) { tasks++; - return function () { - cb && cb.apply(this, arguments); + return function (...args) { + cb && cb.apply(this, args); tasks--; - if (tasks == 0) onAllTasksFinished(); + if (tasks === 0) onAllTasksFinished(); }; } -function onAllTasksFinished() { - hooks.aCallAll('restartServer', {}, () => {}); -} - exports.uninstall = async (pluginName, cb = null) => { cb = wrapTaskCb(cb); try { @@ -60,7 +62,7 @@ exports.install = async (pluginName, cb = null) => { exports.availablePlugins = null; let cacheTimestamp = 0; -exports.getAvailablePlugins = function (maxCacheAge) { +exports.getAvailablePlugins = (maxCacheAge) => { const nowTimestamp = Math.round(Date.now() / 1000); return new Promise((resolve, reject) => { @@ -87,31 +89,33 @@ exports.getAvailablePlugins = function (maxCacheAge) { }; -exports.search = function (searchTerm, maxCacheAge) { - return exports.getAvailablePlugins(maxCacheAge).then((results) => { - const res = {}; +exports.search = (searchTerm, maxCacheAge) => exports.getAvailablePlugins(maxCacheAge).then( + (results) => { + const res = {}; - if (searchTerm) { - searchTerm = searchTerm.toLowerCase(); - } - - for (const pluginName in results) { - // for every available plugin - if (pluginName.indexOf(plugins.prefix) != 0) continue; // TODO: Also search in keywords here! - - if (searchTerm && !~results[pluginName].name.toLowerCase().indexOf(searchTerm) && - (typeof results[pluginName].description !== 'undefined' && !~results[pluginName].description.toLowerCase().indexOf(searchTerm)) - ) { - if (typeof results[pluginName].description === 'undefined') { - console.debug('plugin without Description: %s', results[pluginName].name); - } - - continue; + if (searchTerm) { + searchTerm = searchTerm.toLowerCase(); } - res[pluginName] = results[pluginName]; - } + for (const pluginName in results) { + // for every available plugin + // TODO: Also search in keywords here! + if (pluginName.indexOf(plugins.prefix) !== 0) continue; - return res; - }); -}; + if (searchTerm && !~results[pluginName].name.toLowerCase().indexOf(searchTerm) && + (typeof results[pluginName].description !== 'undefined' && + !~results[pluginName].description.toLowerCase().indexOf(searchTerm)) + ) { + if (typeof results[pluginName].description === 'undefined') { + console.debug('plugin without Description: %s', results[pluginName].name); + } + + continue; + } + + res[pluginName] = results[pluginName]; + } + + return res; + } +); diff --git a/src/static/js/pluginfw/plugin_defs.js b/src/static/js/pluginfw/plugin_defs.js index 95bbcb95c..768d99c3e 100644 --- a/src/static/js/pluginfw/plugin_defs.js +++ b/src/static/js/pluginfw/plugin_defs.js @@ -1,3 +1,5 @@ +'use strict'; + // This module contains processed plugin definitions. The data structures in this file are set by // plugins.js (server) or client_plugins.js (client). diff --git a/src/static/js/pluginfw/plugins.js b/src/static/js/pluginfw/plugins.js index 52fbdd271..4acdee7bd 100644 --- a/src/static/js/pluginfw/plugins.js +++ b/src/static/js/pluginfw/plugins.js @@ -1,6 +1,7 @@ +'use strict'; + const fs = require('fs').promises; const hooks = require('./hooks'); -const npm = require('npm/lib/npm.js'); const readInstalled = require('./read-installed.js'); const path = require('path'); const tsort = require('./tsort'); @@ -13,11 +14,9 @@ const defs = require('./plugin_defs'); exports.prefix = 'ep_'; -exports.formatPlugins = function () { - return _.keys(defs.plugins).join(', '); -}; +exports.formatPlugins = () => Object.keys(defs.plugins).join(', '); -exports.formatPluginsWithVersion = function () { +exports.formatPluginsWithVersion = () => { const plugins = []; _.forEach(defs.plugins, (plugin) => { if (plugin.package.name !== 'ep_etherpad-lite') { @@ -28,17 +27,16 @@ exports.formatPluginsWithVersion = function () { return plugins.join(', '); }; -exports.formatParts = function () { - return _.map(defs.parts, (part) => part.full_name).join('\n'); -}; +exports.formatParts = () => _.map(defs.parts, (part) => part.full_name).join('\n'); -exports.formatHooks = function (hook_set_name) { +exports.formatHooks = (hook_set_name) => { const res = []; const hooks = pluginUtils.extractHooks(defs.parts, hook_set_name || 'hooks'); _.chain(hooks).keys().forEach((hook_name) => { _.forEach(hooks[hook_name], (hook) => { - res.push(`
${hook.hook_name}
${hook.hook_fn_name} from ${hook.part.full_name}
`); + res.push(`
${hook.hook_name}
${hook.hook_fn_name} ` + + `from ${hook.part.full_name}
`); }); }); return `
${res.join('\n')}
`; @@ -57,7 +55,7 @@ const callInit = async () => { })); }; -exports.pathNormalization = function (part, hook_fn_name, hook_name) { +exports.pathNormalization = (part, hook_fn_name, hook_name) => { const tmp = hook_fn_name.split(':'); // hook_fn_name might be something like 'C:\\foo.js:myFunc'. // If there is a single colon assume it's 'filename:funcname' not 'C:\\filename'. const functionName = (tmp.length > 1 ? tmp.pop() : null) || hook_name; @@ -67,7 +65,7 @@ exports.pathNormalization = function (part, hook_fn_name, hook_name) { return `${fileName}:${functionName}`; }; -exports.update = async function () { +exports.update = async () => { const packages = await exports.getPackages(); const parts = {}; // Key is full name. sortParts converts this into a topologically sorted array. const plugins = {}; @@ -83,13 +81,14 @@ exports.update = async function () { await callInit(); }; -exports.getPackages = async function () { - // Load list of installed NPM packages, flatten it to a list, and filter out only packages with names that +exports.getPackages = async () => { + // Load list of installed NPM packages, flatten it to a list, + // and filter out only packages with names that const dir = settings.root; const data = await util.promisify(readInstalled)(dir); const packages = {}; - function flatten(deps) { + const flatten = (deps) => { _.chain(deps).keys().each((name) => { if (name.indexOf(exports.prefix) === 0) { packages[name] = _.clone(deps[name]); @@ -102,7 +101,7 @@ exports.getPackages = async function () { // I don't think we need recursion // if (deps[name].dependencies !== undefined) flatten(deps[name].dependencies); }); - } + }; const tmp = {}; tmp[data.name] = data; @@ -110,7 +109,7 @@ exports.getPackages = async function () { return packages; }; -async function loadPlugin(packages, plugin_name, plugins, parts) { +const loadPlugin = async (packages, plugin_name, plugins, parts) => { const plugin_path = path.resolve(packages[plugin_name].path, 'ep.json'); try { const data = await fs.readFile(plugin_path); @@ -129,9 +128,9 @@ async function loadPlugin(packages, plugin_name, plugins, parts) { } catch (er) { console.error(`Unable to load plugin definition file ${plugin_path}`); } -} +}; -function partsToParentChildList(parts) { +const partsToParentChildList = (parts) => { const res = []; _.chain(parts).keys().forEach((name) => { _.each(parts[name].post || [], (child_name) => { @@ -145,15 +144,9 @@ function partsToParentChildList(parts) { } }); return res; -} +}; // Used only in Node, so no need for _ -function sortParts(parts) { - return tsort( - partsToParentChildList(parts) - ).filter( - (name) => parts[name] !== undefined - ).map( - (name) => parts[name] - ); -} +const sortParts = (parts) => tsort(partsToParentChildList(parts)) + .filter((name) => parts[name] !== undefined) + .map((name) => parts[name]); diff --git a/src/static/js/pluginfw/shared.js b/src/static/js/pluginfw/shared.js index 749706812..981cd2558 100644 --- a/src/static/js/pluginfw/shared.js +++ b/src/static/js/pluginfw/shared.js @@ -1,3 +1,4 @@ +'use strict'; const _ = require('underscore'); const defs = require('./plugin_defs'); @@ -8,13 +9,13 @@ const disabledHookReasons = { }, }; -function loadFn(path, hookName) { +const loadFn = (path, hookName) => { let functionName; const parts = path.split(':'); // on windows: C:\foo\bar:xyz - if (parts[0].length == 1) { - if (parts.length == 3) { + if (parts[0].length === 1) { + if (parts.length === 3) { functionName = parts.pop(); } path = parts.join(':'); @@ -30,9 +31,9 @@ function loadFn(path, hookName) { fn = fn[name]; }); return fn; -} +}; -function extractHooks(parts, hook_set_name, normalizer) { +const extractHooks = (parts, hook_set_name, normalizer) => { const hooks = {}; _.each(parts, (part) => { _.chain(part[hook_set_name] || {}) @@ -50,20 +51,23 @@ function extractHooks(parts, hook_set_name, normalizer) { const disabledReason = (disabledHookReasons[hook_set_name] || {})[hook_name]; if (disabledReason) { - console.error(`Hook ${hook_set_name}/${hook_name} is disabled. Reason: ${disabledReason}`); + console.error( + `Hook ${hook_set_name}/${hook_name} is disabled. Reason: ${disabledReason}`); console.error(`The hook function ${hook_fn_name} from plugin ${part.plugin} ` + - 'will never be called, which may cause the plugin to fail'); - console.error(`Please update the ${part.plugin} plugin to not use the ${hook_name} hook`); + 'will never be called, which may cause the plugin to fail'); + console.error( + `Please update the ${part.plugin} plugin to not use the ${hook_name} hook`); return; } - + let hook_fn; try { - var hook_fn = loadFn(hook_fn_name, hook_name); + hook_fn = loadFn(hook_fn_name, hook_name); if (!hook_fn) { - throw 'Not a function'; + throw new Error('Not a function'); } } catch (exc) { - console.error(`Failed to load '${hook_fn_name}' for '${part.full_name}/${hook_set_name}/${hook_name}': ${exc.toString()}`); + console.error(`Failed to load '${hook_fn_name}' for ` + + `'${part.full_name}/${hook_set_name}/${hook_name}': ${exc.toString()}`); } if (hook_fn) { if (hooks[hook_name] == null) hooks[hook_name] = []; @@ -72,7 +76,7 @@ function extractHooks(parts, hook_set_name, normalizer) { }); }); return hooks; -} +}; exports.extractHooks = extractHooks; @@ -88,10 +92,10 @@ exports.extractHooks = extractHooks; * No plugins: [] * Some plugins: [ 'ep_adminpads', 'ep_add_buttons', 'ep_activepads' ] */ -exports.clientPluginNames = function () { +exports.clientPluginNames = () => { const client_plugin_names = _.uniq( defs.parts - .filter((part) => part.hasOwnProperty('client_hooks')) + .filter((part) => Object.prototype.hasOwnProperty.call(part, 'client_hooks')) .map((part) => `plugin-${part.plugin}`) ); From 5b701b97c3969486affef8b4a4e6ab9bc096c79b Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 1 Feb 2021 15:21:50 +0100 Subject: [PATCH 03/31] Localisation updates from https://translatewiki.net. --- src/locales/gl.json | 69 +++++++++++++++++++++++++++++++++++++-------- src/locales/pt.json | 44 ++++++++++++++++++++++++++--- 2 files changed, 97 insertions(+), 16 deletions(-) diff --git a/src/locales/gl.json b/src/locales/gl.json index 406c99521..02a04f563 100644 --- a/src/locales/gl.json +++ b/src/locales/gl.json @@ -2,12 +2,47 @@ "@metadata": { "authors": [ "Elisardojm", + "Ghose", "Toliño" ] }, + "admin.page-title": "Panel de administración - Etherpad", + "admin_plugins": "Xestor de complementos", + "admin_plugins.available": "Complementos dispoñibles", + "admin_plugins.available_not-found": "Non se atopan complementos.", + "admin_plugins.available_fetching": "Obtendo...", + "admin_plugins.available_install.value": "Instalar", + "admin_plugins.available_search.placeholder": "Buscar complementos para instalar", + "admin_plugins.description": "Descrición", + "admin_plugins.installed": "Complementos instalados", + "admin_plugins.installed_fetching": "Obtendo os complementos instalados...", + "admin_plugins.installed_nothing": "Aínda non instalaches ningún complemento.", + "admin_plugins.installed_uninstall.value": "Desinstalar", + "admin_plugins.last-update": "Última actualización", + "admin_plugins.name": "Nome", + "admin_plugins.page-title": "Xestos de complementos - Etherpad", + "admin_plugins.version": "Versión", + "admin_plugins_info": "Información para resolver problemas", + "admin_plugins_info.hooks": "Ganchos instalados", + "admin_plugins_info.hooks_client": "Ganchos do lado do cliente", + "admin_plugins_info.hooks_server": "Ganchos do lado do servidor", + "admin_plugins_info.parts": "Partes instaladas", + "admin_plugins_info.plugins": "Complementos instalados", + "admin_plugins_info.page-title": "Información do complemento - Etherpad", + "admin_plugins_info.version": "Versión de Etherpad", + "admin_plugins_info.version_latest": "Última versión dispoñible", + "admin_plugins_info.version_number": "Número da versión", + "admin_settings": "Axustes", + "admin_settings.current": "Configuración actual", + "admin_settings.current_example-devel": "Modelo de exemplo dos axustes de desenvolvemento", + "admin_settings.current_example-prod": "Modelo de exemplo dos axustes en produción", + "admin_settings.current_restart.value": "Reiniciar Etherpad", + "admin_settings.current_save.value": "Gardar axustes", + "admin_settings.page-title": "Axustes - Etherpad", "index.newPad": "Novo documento", - "index.createOpenPad": "ou cree/abra un documento co nome:", - "pad.toolbar.bold.title": "Negra (Ctrl-B)", + "index.createOpenPad": "ou crea/abre un documento co nome:", + "index.openPad": "abrir un Pad existente co nome:", + "pad.toolbar.bold.title": "Resaltado (Ctrl-B)", "pad.toolbar.italic.title": "Cursiva (Ctrl-I)", "pad.toolbar.underline.title": "Subliñar (Ctrl-U)", "pad.toolbar.strikethrough.title": "Riscar (Ctrl+5)", @@ -17,28 +52,30 @@ "pad.toolbar.unindent.title": "Sen sangría (Maiús.+TAB)", "pad.toolbar.undo.title": "Desfacer (Ctrl-Z)", "pad.toolbar.redo.title": "Refacer (Ctrl-Y)", - "pad.toolbar.clearAuthorship.title": "Limpar as cores de identificación dos autores (Ctrl+Shift+C)", + "pad.toolbar.clearAuthorship.title": "Eliminar as cores que identifican ás autoras (Ctrl+Shift+C)", "pad.toolbar.import_export.title": "Importar/Exportar desde/a diferentes formatos de ficheiro", "pad.toolbar.timeslider.title": "Liña do tempo", "pad.toolbar.savedRevision.title": "Gardar a revisión", - "pad.toolbar.settings.title": "Configuracións", + "pad.toolbar.settings.title": "Axustes", "pad.toolbar.embed.title": "Compartir e incorporar este documento", - "pad.toolbar.showusers.title": "Mostrar os usuarios deste documento", + "pad.toolbar.showusers.title": "Mostrar as usuarias deste documento", "pad.colorpicker.save": "Gardar", "pad.colorpicker.cancel": "Cancelar", "pad.loading": "Cargando...", - "pad.noCookie": "Non se puido atopar a cookie. Por favor, habilite as cookies no seu navegador!", - "pad.permissionDenied": "Non ten permiso para acceder a este documento", + "pad.noCookie": "Non se puido atopar a cookie. Por favor, habilita as cookies no teu navegador! A túa sesión e axustes non se gardarán entre visitas. Esto podería deberse a que Etherpad está incluído nalgún iFrame nalgúns navegadores. Asegúrate de que Etherpad está no mesmo subdominio/dominio que o iFrame pai", + "pad.permissionDenied": "Non tes permiso para acceder a este documento", "pad.settings.padSettings": "Configuracións do documento", "pad.settings.myView": "A miña vista", "pad.settings.stickychat": "Chat sempre visible", "pad.settings.chatandusers": "Mostrar o chat e os usuarios", "pad.settings.colorcheck": "Cores de identificación", "pad.settings.linenocheck": "Números de liña", - "pad.settings.rtlcheck": "Quere ler o contido da dereita á esquerda?", + "pad.settings.rtlcheck": "Queres ler o contido da dereita á esquerda?", "pad.settings.fontType": "Tipo de letra:", "pad.settings.fontType.normal": "Normal", "pad.settings.language": "Lingua:", + "pad.settings.about": "Acerca de", + "pad.settings.poweredBy": "Grazas a", "pad.importExport.import_export": "Importar/Exportar", "pad.importExport.import": "Cargar un ficheiro de texto ou documento", "pad.importExport.importSuccessful": "Correcto!", @@ -49,9 +86,9 @@ "pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (Open Document Format)", - "pad.importExport.abiword.innerHTML": "Só pode importar texto simple ou formatos HTML. Para obter máis información sobre as características de importación avanzadas instale AbiWord.", + "pad.importExport.abiword.innerHTML": "Só podes importar texto simple ou formatos HTML. Para obter máis información sobre as características de importación avanzadas instala AbiWord.", "pad.modals.connected": "Conectado.", - "pad.modals.reconnecting": "Reconectando co seu documento...", + "pad.modals.reconnecting": "Reconectando co teu documento...", "pad.modals.forcereconnect": "Forzar a reconexión", "pad.modals.reconnecttimer": "Intentarase reconectar en", "pad.modals.cancel": "Cancelar", @@ -73,6 +110,10 @@ "pad.modals.corruptPad.cause": "Isto pode deberse a unha cofiguración errónea do servidor ou algún outro comportamento inesperado. Póñase en contacto co administrador do servizo.", "pad.modals.deleted": "Borrado.", "pad.modals.deleted.explanation": "Este documento foi eliminado.", + "pad.modals.rateLimited": "Taxa limitada.", + "pad.modals.rateLimited.explanation": "Enviaches demasiadas mensaxes a este documento polo que te desconectamos.", + "pad.modals.rejected.explanation": "O servidor rexeitou unha mensaxe que o teu navegador enviou.", + "pad.modals.rejected.cause": "O servidor podería ter sido actualizado mentras ollabas o documento, ou pode que sexa un fallo de Etherpad. Intenta recargar a páxina.", "pad.modals.disconnected": "Foi desconectado.", "pad.modals.disconnected.explanation": "Perdeuse a conexión co servidor", "pad.modals.disconnected.cause": "O servidor non está dispoñible. Póñase en contacto co administrador do servizo se o problema continúa.", @@ -83,6 +124,9 @@ "pad.chat": "Chat", "pad.chat.title": "Abrir o chat deste documento.", "pad.chat.loadmessages": "Cargar máis mensaxes", + "pad.chat.stick.title": "Pegar a conversa á pantalla", + "pad.chat.writeMessage.placeholder": "Escribe aquí a túa mensaxe", + "timeslider.followContents": "Segue as actualizacións do contido", "timeslider.pageTitle": "Liña do tempo de {{appTitle}}", "timeslider.toolbar.returnbutton": "Volver ao documento", "timeslider.toolbar.authors": "Autores:", @@ -112,7 +156,7 @@ "pad.savedrevs.timeslider": "Pode consultar as revisións gardadas visitando a liña do tempo", "pad.userlist.entername": "Insira o seu nome", "pad.userlist.unnamed": "anónimo", - "pad.editbar.clearcolors": "Quere limpar as cores de identificación dos autores en todo o documento?", + "pad.editbar.clearcolors": "Eliminar as cores relativas aos autores en todo o documento? Non se poderán recuperar", "pad.impexp.importbutton": "Importar agora", "pad.impexp.importing": "Importando...", "pad.impexp.confirmimport": "A importación dun ficheiro ha sobrescribir o texto actual do documento. Está seguro de querer continuar?", @@ -121,5 +165,6 @@ "pad.impexp.uploadFailed": "Houbo un erro ao cargar o ficheiro; inténteo de novo", "pad.impexp.importfailed": "Fallou a importación", "pad.impexp.copypaste": "Copie e pegue", - "pad.impexp.exportdisabled": "A exportación en formato {{type}} está desactivada. Póñase en contacto co administrador do sistema se quere máis detalles." + "pad.impexp.exportdisabled": "A exportación en formato {{type}} está desactivada. Póñase en contacto co administrador do sistema se quere máis detalles.", + "pad.impexp.maxFileSize": "Ficheiro demasiado granda. Contacta coa administración para aumentar o tamaño permitido para importacións" } diff --git a/src/locales/pt.json b/src/locales/pt.json index d9faa3ba0..82972333e 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -4,6 +4,7 @@ "Athena in Wonderland", "Cainamarques", "GoEThe", + "Guilha", "Hamilton Abreu", "Imperadeiro98", "Luckas", @@ -16,9 +17,42 @@ "Waldyrious" ] }, + "admin.page-title": "Painel do administrador - Etherpad", + "admin_plugins": "Gestor de plugins", + "admin_plugins.available": "Plugins disponíveis", + "admin_plugins.available_not-found": "Não foram encontrados plugins.", + "admin_plugins.available_fetching": "A obter...", + "admin_plugins.available_install.value": "Instalar", + "admin_plugins.available_search.placeholder": "Procura plugins para instalar", + "admin_plugins.description": "Descrição", + "admin_plugins.installed": "Plugins instalados", + "admin_plugins.installed_fetching": "A obter plugins instalados...", + "admin_plugins.installed_nothing": "Não instalas-te nenhum plugin ainda.", + "admin_plugins.installed_uninstall.value": "Desinstalar", + "admin_plugins.last-update": "Ultima atualização", + "admin_plugins.name": "Nome", + "admin_plugins.page-title": "Gestor de plugins - Etherpad", + "admin_plugins.version": "Versão", + "admin_plugins_info": "Informação de resolução de problemas", + "admin_plugins_info.hooks": "Hooks instalados", + "admin_plugins_info.hooks_client": "Hooks do lado-do-cliente", + "admin_plugins_info.hooks_server": "Hooks do lado-do-servidor", + "admin_plugins_info.parts": "Partes instaladas", + "admin_plugins_info.plugins": "Plugins instalados", + "admin_plugins_info.page-title": "Informação do plugin - Etherpad", + "admin_plugins_info.version": "Versão do Etherpad", + "admin_plugins_info.version_latest": "Última versão disponível", + "admin_plugins_info.version_number": "Número de versão", + "admin_settings": "Definições", + "admin_settings.current": "Configuração atual", + "admin_settings.current_example-devel": "Exemplo do modo de Desenvolvedor", + "admin_settings.current_example-prod": "Exemplo do modo de Produção", + "admin_settings.current_restart.value": "Reiniciar Etherpad", + "admin_settings.current_save.value": "Guardar Definições", + "admin_settings.page-title": "Definições - Etherpad", "index.newPad": "Nova Nota", - "index.createOpenPad": "ou crie/abra uma nota com o nome:", - "index.openPad": "abrir uma «Nota» existente com o nome:", + "index.createOpenPad": "ou cria/abre uma nota com o nome:", + "index.openPad": "abrir uma Nota existente com o nome:", "pad.toolbar.bold.title": "Negrito (Ctrl+B)", "pad.toolbar.italic.title": "Itálico (Ctrl+I)", "pad.toolbar.underline.title": "Sublinhado (Ctrl+U)", @@ -26,7 +60,7 @@ "pad.toolbar.ol.title": "Lista ordenada (Ctrl+Shift+N)", "pad.toolbar.ul.title": "Lista desordenada (Ctrl+Shift+L)", "pad.toolbar.indent.title": "Indentar (TAB)", - "pad.toolbar.unindent.title": "Remover indentação (Shift+TAB)", + "pad.toolbar.unindent.title": "Indentação (Shift+TAB)", "pad.toolbar.undo.title": "Desfazer (Ctrl+Z)", "pad.toolbar.redo.title": "Refazer (Ctrl+Y)", "pad.toolbar.clearAuthorship.title": "Limpar cores de autoria (Ctrl+Shift+C)", @@ -65,7 +99,7 @@ "pad.importExport.exportopen": "ODF (Open Document Format)", "pad.importExport.abiword.innerHTML": "Só pode fazer importações de texto não formatado ou com formato HTML. Para funcionalidades de importação de texto mais avançadas, instale AbiWord ou LibreOffice, por favor.", "pad.modals.connected": "Ligado.", - "pad.modals.reconnecting": "A restabelecer ligação ao seu bloco…", + "pad.modals.reconnecting": "A restabelecer ligação à nota…", "pad.modals.forcereconnect": "Forçar restabelecimento de ligação", "pad.modals.reconnecttimer": "A tentar restabelecer ligação", "pad.modals.cancel": "Cancelar", @@ -89,6 +123,8 @@ "pad.modals.deleted.explanation": "Esta nota foi removida.", "pad.modals.rateLimited": "Limitado.", "pad.modals.rateLimited.explanation": "Enviou demasiadas mensagens para este pad, por isso foi desligado.", + "pad.modals.rejected.explanation": "O servidor rejeitou a mensagem que foi enviada pelo teu navegador.", + "pad.modals.rejected.cause": "O server foi atualizado enquanto estávas a ver esta nota, ou talvez seja apenas um bug do Etherpad. Tenta recarregar a página.", "pad.modals.disconnected": "Você foi desligado.", "pad.modals.disconnected.explanation": "A ligação ao servidor foi perdida", "pad.modals.disconnected.cause": "O servidor pode estar indisponível. Por favor, notifique o administrador de serviço se isto continuar a acontecer.", From 9987fab574d0e5b1291e5a3e88351b2ec35a4325 Mon Sep 17 00:00:00 2001 From: John McLear Date: Mon, 1 Feb 2021 09:47:42 +0000 Subject: [PATCH 04/31] lint: low hanging bin/doc/*.js --- bin/doc/generate.js | 28 +++++++++++---------- bin/doc/html.js | 42 +++++++++++++++---------------- bin/doc/json.js | 59 ++++++++++++++++++++++---------------------- bin/doc/package.json | 2 +- 4 files changed, 67 insertions(+), 64 deletions(-) diff --git a/bin/doc/generate.js b/bin/doc/generate.js index 803f5017e..d04468a8b 100644 --- a/bin/doc/generate.js +++ b/bin/doc/generate.js @@ -1,4 +1,7 @@ #!/usr/bin/env node + +'use strict'; + // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a @@ -20,7 +23,6 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -const marked = require('marked'); const fs = require('fs'); const path = require('path'); @@ -33,12 +35,12 @@ let template = null; let inputFile = null; args.forEach((arg) => { - if (!arg.match(/^\-\-/)) { + if (!arg.match(/^--/)) { inputFile = arg; - } else if (arg.match(/^\-\-format=/)) { - format = arg.replace(/^\-\-format=/, ''); - } else if (arg.match(/^\-\-template=/)) { - template = arg.replace(/^\-\-template=/, ''); + } else if (arg.match(/^--format=/)) { + format = arg.replace(/^--format=/, ''); + } else if (arg.match(/^--template=/)) { + template = arg.replace(/^--template=/, ''); } }); @@ -56,11 +58,11 @@ fs.readFile(inputFile, 'utf8', (er, input) => { }); -const includeExpr = /^@include\s+([A-Za-z0-9-_\/]+)(?:\.)?([a-zA-Z]*)$/gmi; +const includeExpr = /^@include\s+([A-Za-z0-9-_/]+)(?:\.)?([a-zA-Z]*)$/gmi; const includeData = {}; -function processIncludes(inputFile, input, cb) { +const processIncludes = (inputFile, input, cb) => { const includes = input.match(includeExpr); - if (includes === null) return cb(null, input); + if (includes == null) return cb(null, input); let errState = null; console.error(includes); let incCount = includes.length; @@ -70,7 +72,7 @@ function processIncludes(inputFile, input, cb) { let fname = include.replace(/^@include\s+/, ''); if (!fname.match(/\.md$/)) fname += '.md'; - if (includeData.hasOwnProperty(fname)) { + if (Object.prototype.hasOwnProperty.call(includeData, fname)) { input = input.split(include).join(includeData[fname]); incCount--; if (incCount === 0) { @@ -94,10 +96,10 @@ function processIncludes(inputFile, input, cb) { }); }); }); -} +}; -function next(er, input) { +const next = (er, input) => { if (er) throw er; switch (format) { case 'json': @@ -117,4 +119,4 @@ function next(er, input) { default: throw new Error(`Invalid format: ${format}`); } -} +}; diff --git a/bin/doc/html.js b/bin/doc/html.js index 26cf3f185..2c38aec23 100644 --- a/bin/doc/html.js +++ b/bin/doc/html.js @@ -1,3 +1,5 @@ +'use strict'; + // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a @@ -23,17 +25,17 @@ const fs = require('fs'); const marked = require('marked'); const path = require('path'); -module.exports = toHTML; -function toHTML(input, filename, template, cb) { +const toHTML = (input, filename, template, cb) => { const lexed = marked.lexer(input); fs.readFile(template, 'utf8', (er, template) => { if (er) return cb(er); render(lexed, filename, template, cb); }); -} +}; +module.exports = toHTML; -function render(lexed, filename, template, cb) { +const render = (lexed, filename, template, cb) => { // get the section const section = getSection(lexed); @@ -52,23 +54,23 @@ function render(lexed, filename, template, cb) { // content has to be the last thing we do with // the lexed tokens, because it's destructive. - content = marked.parser(lexed); + const content = marked.parser(lexed); template = template.replace(/__CONTENT__/g, content); cb(null, template); }); -} +}; // just update the list item text in-place. // lists that come right after a heading are what we're after. -function parseLists(input) { +const parseLists = (input) => { let state = null; let depth = 0; const output = []; output.links = input.links; input.forEach((tok) => { - if (state === null) { + if (state == null) { if (tok.type === 'heading') { state = 'AFTERHEADING'; } @@ -112,29 +114,27 @@ function parseLists(input) { }); return output; -} +}; -function parseListItem(text) { - text = text.replace(/\{([^\}]+)\}/, '$1'); +const parseListItem = (text) => { + text = text.replace(/\{([^}]+)\}/, '$1'); // XXX maybe put more stuff here? return text; -} +}; // section is just the first heading -function getSection(lexed) { - const section = ''; +const getSection = (lexed) => { for (let i = 0, l = lexed.length; i < l; i++) { const tok = lexed[i]; if (tok.type === 'heading') return tok.text; } return ''; -} +}; -function buildToc(lexed, filename, cb) { - const indent = 0; +const buildToc = (lexed, filename, cb) => { let toc = []; let depth = 0; lexed.forEach((tok) => { @@ -155,18 +155,18 @@ function buildToc(lexed, filename, cb) { toc = marked.parse(toc.join('\n')); cb(null, toc); -} +}; const idCounters = {}; -function getId(text) { +const getId = (text) => { text = text.toLowerCase(); text = text.replace(/[^a-z0-9]+/g, '_'); text = text.replace(/^_+|_+$/, ''); text = text.replace(/^([^a-z])/, '_$1'); - if (idCounters.hasOwnProperty(text)) { + if (Object.prototype.hasOwnProperty.call(idCounters, text)) { text += `_${++idCounters[text]}`; } else { idCounters[text] = 0; } return text; -} +}; diff --git a/bin/doc/json.js b/bin/doc/json.js index 3ce62a301..c71611e5f 100644 --- a/bin/doc/json.js +++ b/bin/doc/json.js @@ -1,3 +1,4 @@ +'use strict'; // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a @@ -26,7 +27,7 @@ module.exports = doJSON; const marked = require('marked'); -function doJSON(input, filename, cb) { +const doJSON = (input, filename, cb) => { const root = {source: filename}; const stack = [root]; let depth = 0; @@ -40,7 +41,7 @@ function doJSON(input, filename, cb) { // // This is for cases where the markdown semantic structure is lacking. if (type === 'paragraph' || type === 'html') { - const metaExpr = /\n*/g; + const metaExpr = /\n*/g; text = text.replace(metaExpr, (_0, k, v) => { current[k.trim()] = v.trim(); return ''; @@ -146,7 +147,7 @@ function doJSON(input, filename, cb) { } return cb(null, root); -} +}; // go from something like this: @@ -191,7 +192,7 @@ function doJSON(input, filename, cb) { // desc: 'whether or not to send output to parent\'s stdio.', // default: 'false' } ] } ] -function processList(section) { +const processList = (section) => { const list = section.list; const values = []; let current; @@ -203,13 +204,13 @@ function processList(section) { if (type === 'space') return; if (type === 'list_item_start') { if (!current) { - var n = {}; + const n = {}; values.push(n); current = n; } else { current.options = current.options || []; stack.push(current); - var n = {}; + const n = {}; current.options.push(n); current = n; } @@ -283,11 +284,11 @@ function processList(section) { // section.listParsed = values; delete section.list; -} +}; // textRaw = "someobject.someMethod(a, [b=100], [c])" -function parseSignature(text, sig) { +const parseSignature = (text, sig) => { let params = text.match(paramExpr); if (!params) return; params = params[1]; @@ -322,10 +323,10 @@ function parseSignature(text, sig) { if (optional) param.optional = true; if (def !== undefined) param.default = def; }); -} +}; -function parseListItem(item) { +const parseListItem = (item) => { if (item.options) item.options.forEach(parseListItem); if (!item.textRaw) return; @@ -341,7 +342,7 @@ function parseListItem(item) { item.name = 'return'; text = text.replace(retExpr, ''); } else { - const nameExpr = /^['`"]?([^'`": \{]+)['`"]?\s*:?\s*/; + const nameExpr = /^['`"]?([^'`": {]+)['`"]?\s*:?\s*/; const name = text.match(nameExpr); if (name) { item.name = name[1]; @@ -358,7 +359,7 @@ function parseListItem(item) { } text = text.trim(); - const typeExpr = /^\{([^\}]+)\}/; + const typeExpr = /^\{([^}]+)\}/; const type = text.match(typeExpr); if (type) { item.type = type[1]; @@ -376,10 +377,10 @@ function parseListItem(item) { text = text.replace(/^\s*-\s*/, ''); text = text.trim(); if (text) item.desc = text; -} +}; -function finishSection(section, parent) { +const finishSection = (section, parent) => { if (!section || !parent) { throw new Error(`Invalid finishSection call\n${ JSON.stringify(section)}\n${ @@ -479,50 +480,50 @@ function finishSection(section, parent) { parent[plur] = parent[plur] || []; parent[plur].push(section); -} +}; // Not a general purpose deep copy. // But sufficient for these basic things. -function deepCopy(src, dest) { +const deepCopy = (src, dest) => { Object.keys(src).filter((k) => !dest.hasOwnProperty(k)).forEach((k) => { dest[k] = deepCopy_(src[k]); }); -} +}; -function deepCopy_(src) { +const deepCopy_ = (src) => { if (!src) return src; if (Array.isArray(src)) { - var c = new Array(src.length); + const c = new Array(src.length); src.forEach((v, i) => { c[i] = deepCopy_(v); }); return c; } if (typeof src === 'object') { - var c = {}; + const c = {}; Object.keys(src).forEach((k) => { c[k] = deepCopy_(src[k]); }); return c; } return src; -} +}; // these parse out the contents of an H# tag const eventExpr = /^Event(?::|\s)+['"]?([^"']+).*$/i; const classExpr = /^Class:\s*([^ ]+).*?$/i; -const propExpr = /^(?:property:?\s*)?[^\.]+\.([^ \.\(\)]+)\s*?$/i; -const braceExpr = /^(?:property:?\s*)?[^\.\[]+(\[[^\]]+\])\s*?$/i; +const propExpr = /^(?:property:?\s*)?[^.]+\.([^ .()]+)\s*?$/i; +const braceExpr = /^(?:property:?\s*)?[^.[]+(\[[^\]]+\])\s*?$/i; const classMethExpr = - /^class\s*method\s*:?[^\.]+\.([^ \.\(\)]+)\([^\)]*\)\s*?$/i; + /^class\s*method\s*:?[^.]+\.([^ .()]+)\([^)]*\)\s*?$/i; const methExpr = - /^(?:method:?\s*)?(?:[^\.]+\.)?([^ \.\(\)]+)\([^\)]*\)\s*?$/i; -const newExpr = /^new ([A-Z][a-z]+)\([^\)]*\)\s*?$/; -var paramExpr = /\((.*)\);?$/; + /^(?:method:?\s*)?(?:[^.]+\.)?([^ .()]+)\([^)]*\)\s*?$/i; +const newExpr = /^new ([A-Z][a-z]+)\([^)]*\)\s*?$/; +const paramExpr = /\((.*)\);?$/; -function newSection(tok) { +const newSection = (tok) => { const section = {}; // infer the type from the text. const text = section.textRaw = tok.text; @@ -551,4 +552,4 @@ function newSection(tok) { section.name = text; } return section; -} +}; diff --git a/bin/doc/package.json b/bin/doc/package.json index 1a29f1b1c..2f027616c 100644 --- a/bin/doc/package.json +++ b/bin/doc/package.json @@ -4,7 +4,7 @@ "description": "Internal tool for generating Node.js API docs", "version": "0.0.0", "engines": { - "node": ">=0.6.10" + "node": ">=10.17.0" }, "dependencies": { "marked": "0.8.2" From 759e2aaec39a8b6f6e15409e6a9750c49f55627c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 1 Feb 2021 14:43:09 -0500 Subject: [PATCH 05/31] lint: Use node config for tests/frontend/travis, tests/ratelimit The files in these directories contain test drivers, not tests. --- package.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 03cb677ba..de9298807 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,9 @@ "tests/**/*" ], "excludedFiles": [ - "**/.eslintrc.js" + "**/.eslintrc.js", + "tests/frontend/travis/**/*", + "tests/ratelimit/**/*" ], "extends": "etherpad/tests", "rules": { @@ -75,7 +77,8 @@ "tests/frontend/**/*" ], "excludedFiles": [ - "**/.eslintrc.js" + "**/.eslintrc.js", + "tests/frontend/travis/**/*" ], "extends": "etherpad/tests/frontend", "overrides": [ @@ -92,6 +95,12 @@ } } ] + }, + { + "files": [ + "tests/frontend/travis/**/*" + ], + "extends": "etherpad/node" } ], "root": true From 915849b3197158427c1a755e2d60e38822dc16a9 Mon Sep 17 00:00:00 2001 From: John McLear Date: Mon, 1 Feb 2021 20:23:14 +0000 Subject: [PATCH 06/31] Low hanging lint frontend tests (#4695) * lint: low hanging specs/alphabet.js * lint: low hanging specs/authorship_of_editions.js * lint: low hanging specs/bold.js * lint: low hanging specs/caret.js * lint: low hanging specs/change_user_color.js * lint: low hanging specs/change_user_name.js * lint: low hanging specs/chat.js * lint: low hanging specs/chat_load_messages.js * lint: low hanging specs/clear_authorship_colors.js * lint: low hanging specs/delete.js * lint: low hanging specs/drag_and_drop.js * lint: low hanging specs/embed_value.js * lint: low hanging specs/enter.js * lint: low hanging specs/font_type.js * lint: low hanging specs/helper.js * lint: low hanging specs/importexport.js * lint: low hanging specs/importindents.js * lint: low hanging specs/indentation.js * lint: low hanging specs/italic.js * lint: low hanging specs/language.js * lint: low hanging specs/multiple_authors_clear_authorship_colors.js * lint: low hanging specs/ordered_list.js * lint: low hanging specs/pad_modal.js * lint: low hanging specs/redo.js * lint: low hanging specs/responsiveness.js * lint: low hanging specs/select_formatting_buttons.js * lint: low hanging specs/strikethrough.js * lint: low hanging specs/timeslider.js * lint: low hanging specs/timeslider_labels.js * lint: low hanging specs/timeslider_numeric_padID.js * lint: low hanging specs/timeslider_revisions.js * lint: low hanging specs/undo.js * lint: low hanging specs/unordered_list.js * lint: low hanging specs/xxauto_reconnect.js * lint: attempt to do remote_runner.js * lint: helper linting * lint: rate limit linting * use constructor for Event to make eslint happier * for squash: lint fix refinements * for squash: lint fix refinements Co-authored-by: Richard Hansen --- tests/frontend/helper.js | 4 +- tests/frontend/helper/methods.js | 8 +- tests/frontend/helper/ui.js | 2 + tests/frontend/specs/alphabet.js | 3 +- .../frontend/specs/authorship_of_editions.js | 26 +- tests/frontend/specs/bold.js | 7 +- tests/frontend/specs/caret.js | 67 +++-- tests/frontend/specs/change_user_color.js | 16 +- tests/frontend/specs/change_user_name.js | 2 + tests/frontend/specs/chat.js | 19 +- tests/frontend/specs/chat_load_messages.js | 18 +- .../frontend/specs/clear_authorship_colors.js | 35 +-- tests/frontend/specs/delete.js | 8 +- tests/frontend/specs/drag_and_drop.js | 29 +- tests/frontend/specs/embed_value.js | 2 + tests/frontend/specs/enter.js | 9 +- tests/frontend/specs/font_type.js | 3 +- tests/frontend/specs/helper.js | 28 +- tests/frontend/specs/importexport.js | 271 ++++++++++++------ tests/frontend/specs/importindents.js | 82 ++++-- tests/frontend/specs/indentation.js | 39 +-- tests/frontend/specs/italic.js | 9 +- tests/frontend/specs/language.js | 18 +- ...ultiple_authors_clear_authorship_colors.js | 9 +- tests/frontend/specs/ordered_list.js | 27 +- tests/frontend/specs/pad_modal.js | 12 +- tests/frontend/specs/redo.js | 7 +- tests/frontend/specs/responsiveness.js | 37 ++- .../specs/select_formatting_buttons.js | 9 +- tests/frontend/specs/strikethrough.js | 4 +- tests/frontend/specs/timeslider.js | 3 +- tests/frontend/specs/timeslider_labels.js | 4 +- .../specs/timeslider_numeric_padID.js | 2 + tests/frontend/specs/timeslider_revisions.js | 48 ++-- tests/frontend/specs/undo.js | 5 +- tests/frontend/specs/unordered_list.js | 5 +- tests/frontend/specs/xxauto_reconnect.js | 2 + tests/frontend/travis/remote_runner.js | 63 ++-- tests/ratelimit/send_changesets.js | 10 +- 39 files changed, 595 insertions(+), 357 deletions(-) diff --git a/tests/frontend/helper.js b/tests/frontend/helper.js index 37c5af3b1..c38175fe1 100644 --- a/tests/frontend/helper.js +++ b/tests/frontend/helper.js @@ -1,5 +1,5 @@ 'use strict'; -const helper = {}; // eslint-disable-line +const helper = {}; // eslint-disable-line no-redeclare (function () { let $iframe; const @@ -181,7 +181,7 @@ const helper = {}; // eslint-disable-line }; helper.waitFor = function (conditionFunc, timeoutTime = 1900, intervalTime = 10) { - const deferred = $.Deferred(); // eslint-disable-line + const deferred = new $.Deferred(); const _fail = deferred.fail.bind(deferred); let listenForFail = false; diff --git a/tests/frontend/helper/methods.js b/tests/frontend/helper/methods.js index 4c7fe1204..157ba6aba 100644 --- a/tests/frontend/helper/methods.js +++ b/tests/frontend/helper/methods.js @@ -6,12 +6,12 @@ */ helper.spyOnSocketIO = function () { helper.contentWindow().pad.socket.on('message', (msg) => { - if (msg.type == 'COLLABROOM') { - if (msg.data.type == 'ACCEPT_COMMIT') { + if (msg.type === 'COLLABROOM') { + if (msg.data.type === 'ACCEPT_COMMIT') { helper.commits.push(msg); - } else if (msg.data.type == 'USER_NEWINFO') { + } else if (msg.data.type === 'USER_NEWINFO') { helper.userInfos.push(msg); - } else if (msg.data.type == 'CHAT_MESSAGE') { + } else if (msg.data.type === 'CHAT_MESSAGE') { helper.chatMessages.push(msg); } } diff --git a/tests/frontend/helper/ui.js b/tests/frontend/helper/ui.js index 0f3e64169..7ab8b990d 100644 --- a/tests/frontend/helper/ui.js +++ b/tests/frontend/helper/ui.js @@ -1,3 +1,5 @@ +'use strict'; + /** * the contentWindow is either the normal pad or timeslider * diff --git a/tests/frontend/specs/alphabet.js b/tests/frontend/specs/alphabet.js index a0ad61bdf..cc50e7d87 100644 --- a/tests/frontend/specs/alphabet.js +++ b/tests/frontend/specs/alphabet.js @@ -1,3 +1,5 @@ +'use strict'; + describe('All the alphabet works n stuff', function () { const expectedString = 'abcdefghijklmnopqrstuvwxyz'; @@ -9,7 +11,6 @@ describe('All the alphabet works n stuff', function () { it('when you enter any char it appears right', function (done) { const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // get the first text element out of the inner iframe const firstTextElement = inner$('div').first(); diff --git a/tests/frontend/specs/authorship_of_editions.js b/tests/frontend/specs/authorship_of_editions.js index 6cf14b869..f6f29d491 100644 --- a/tests/frontend/specs/authorship_of_editions.js +++ b/tests/frontend/specs/authorship_of_editions.js @@ -1,3 +1,5 @@ +'use strict'; + describe('author of pad edition', function () { const REGULAR_LINE = 0; const LINE_WITH_ORDERED_LIST = 1; @@ -5,10 +7,11 @@ describe('author of pad edition', function () { // author 1 creates a new pad with some content (regular lines and lists) before(function (done) { - var padId = helper.newPad(() => { + const padId = helper.newPad(() => { // make sure pad has at least 3 lines const $firstLine = helper.padInner$('div').first(); - const threeLines = ['regular line', 'line with ordered list', 'line with unordered list'].join('
'); + const threeLines = ['regular line', 'line with ordered list', 'line with unordered list'] + .join('
'); $firstLine.html(threeLines); // wait for lines to be processed by Etherpad @@ -43,7 +46,8 @@ describe('author of pad edition', function () { setTimeout(() => { // Expire cookie, so author is changed after reloading the pad. // See https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#Example_4_Reset_the_previous_cookie - helper.padChrome$.document.cookie = 'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; + helper.padChrome$.document.cookie = + 'token=foo;expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; helper.newPad(done, padId); }, 1000); @@ -59,24 +63,22 @@ describe('author of pad edition', function () { changeLineAndCheckOnlyThatChangeIsFromThisAuthor(REGULAR_LINE, 'x', done); }); - it('marks only the new content as changes of the second user on a line with ordered list', function (done) { + it('marks only the new content as changes of the second user on a ' + + 'line with ordered list', function (done) { 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) { changeLineAndCheckOnlyThatChangeIsFromThisAuthor(LINE_WITH_UNORDERED_LIST, 'z', done); }); /* ********************** Helper functions ************************ */ - var getLine = function (lineNumber) { - return helper.padInner$('div').eq(lineNumber); - }; + const getLine = (lineNumber) => helper.padInner$('div').eq(lineNumber); - const getAuthorFromClassList = function (classes) { - return classes.find((cls) => cls.startsWith('author')); - }; + const getAuthorFromClassList = (classes) => classes.find((cls) => cls.startsWith('author')); - var changeLineAndCheckOnlyThatChangeIsFromThisAuthor = function (lineNumber, textChange, done) { + const changeLineAndCheckOnlyThatChangeIsFromThisAuthor = (lineNumber, textChange, done) => { // get original author class const classes = getLine(lineNumber).find('span').first().attr('class').split(' '); const originalAuthor = getAuthorFromClassList(classes); diff --git a/tests/frontend/specs/bold.js b/tests/frontend/specs/bold.js index a7c46e1bc..613de4699 100644 --- a/tests/frontend/specs/bold.js +++ b/tests/frontend/specs/bold.js @@ -1,3 +1,5 @@ +'use strict'; + describe('bold button', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -19,7 +21,6 @@ describe('bold button', function () { const $boldButton = chrome$('.buttonicon-bold'); $boldButton.click(); - // ace creates a new dom element when you press a button, so just get the first text element again const $newFirstTextElement = inner$('div').first(); // is there a element now? @@ -36,7 +37,6 @@ describe('bold button', function () { it('makes text bold on keypress', function (done) { const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // get the first text element out of the inner iframe const $firstTextElement = inner$('div').first(); @@ -44,12 +44,11 @@ describe('bold button', function () { // select this text element $firstTextElement.sendkeys('{selectall}'); - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 66; // b inner$('#innerdocbody').trigger(e); - // ace creates a new dom element when you press a button, so just get the first text element again const $newFirstTextElement = inner$('div').first(); // is there a element now? diff --git a/tests/frontend/specs/caret.js b/tests/frontend/specs/caret.js index 1fb8d8aa7..e5ce255b8 100644 --- a/tests/frontend/specs/caret.js +++ b/tests/frontend/specs/caret.js @@ -1,7 +1,9 @@ +'use strict'; + describe('As the caret is moved is the UI properly updated?', function () { + /* let padName; const numberOfRows = 50; - /* //create a new pad before each test run beforeEach(function(cb){ @@ -16,7 +18,8 @@ describe('As the caret is moved is the UI properly updated?', function () { */ /* Tests to do - * Keystroke up (38), down (40), left (37), right (39) with and without special keys IE control / shift + * Keystroke up (38), down (40), left (37), right (39) + * with and without special keys IE control / shift * Page up (33) / down (34) with and without special keys * Page up on the first line shouldn't move the viewport * Down down on the last line shouldn't move the viewport @@ -25,7 +28,9 @@ describe('As the caret is moved is the UI properly updated?', function () { */ /* Challenges - * How do we keep the authors focus on a line if the lines above the author are modified? We should only redraw the user to a location if they are typing and make sure shift and arrow keys aren't redrawing the UI else highlight - copy/paste would get broken + * How do we keep the authors focus on a line if the lines above the author are modified? + * We should only redraw the user to a location if they are typing and make sure shift + * and arrow keys aren't redrawing the UI else highlight - copy/paste would get broken * How can we simulate an edit event in the test framework? */ /* @@ -200,7 +205,8 @@ console.log(inner$); var chrome$ = helper.padChrome$; var numberOfRows = 50; - //ace creates a new dom element when you press a keystroke, so just get the first text element again + // ace creates a new dom element when you press a keystroke, + // so just get the first text element again var $newFirstTextElement = inner$("div").first(); var originalDivHeight = inner$("div").first().css("height"); prepareDocument(numberOfRows, $newFirstTextElement); // N lines into the first div as a target @@ -208,28 +214,33 @@ console.log(inner$); helper.waitFor(function(){ // Wait for the DOM to register the new items return inner$("div").first().text().length == 6; }).done(function(){ // Once the DOM has registered the items - inner$("div").each(function(index){ // Randomize the item heights (replicates images / headings etc) + // Randomize the item heights (replicates images / headings etc) + inner$("div").each(function(index){ var random = Math.floor(Math.random() * (50)) + 20; $(this).css("height", random+"px"); }); console.log(caretPosition(inner$)); var newDivHeight = inner$("div").first().css("height"); - var heightHasChanged = originalDivHeight != newDivHeight; // has the new div height changed from the original div height + // has the new div height changed from the original div height + var heightHasChanged = originalDivHeight != newDivHeight; expect(heightHasChanged).to.be(true); // expect the first line to be blank }); // Is this Element now visible to the pad user? helper.waitFor(function(){ // Wait for the DOM to register the new items - return isScrolledIntoView(inner$("div:nth-child("+numberOfRows+")"), inner$); // Wait for the DOM to scroll into place + // Wait for the DOM to scroll into place + return isScrolledIntoView(inner$("div:nth-child("+numberOfRows+")"), inner$); }).done(function(){ // Once the DOM has registered the items - inner$("div").each(function(index){ // Randomize the item heights (replicates images / headings etc) + // Randomize the item heights (replicates images / headings etc) + inner$("div").each(function(index){ var random = Math.floor(Math.random() * (80 - 20 + 1)) + 20; $(this).css("height", random+"px"); }); var newDivHeight = inner$("div").first().css("height"); - var heightHasChanged = originalDivHeight != newDivHeight; // has the new div height changed from the original div height + // has the new div height changed from the original div height + var heightHasChanged = originalDivHeight != newDivHeight; expect(heightHasChanged).to.be(true); // expect the first line to be blank }); var i = 0; @@ -241,7 +252,8 @@ console.log(inner$); // Does scrolling back up the pad with the up arrow show the correct contents? helper.waitFor(function(){ // Wait for the new position to be in place try{ - return isScrolledIntoView(inner$("div:nth-child("+numberOfRows+")"), inner$); // Wait for the DOM to scroll into place + // Wait for the DOM to scroll into place + return isScrolledIntoView(inner$("div:nth-child("+numberOfRows+")"), inner$); }catch(e){ return false; } @@ -256,7 +268,8 @@ console.log(inner$); // Does scrolling back up the pad with the up arrow show the correct contents? helper.waitFor(function(){ // Wait for the new position to be in place try{ - return isScrolledIntoView(inner$("div:nth-child(0)"), inner$); // Wait for the DOM to scroll into place + // Wait for the DOM to scroll into place + return isScrolledIntoView(inner$("div:nth-child(0)"), inner$); }catch(e){ return false; } @@ -276,7 +289,8 @@ console.log(inner$); // Does scrolling back up the pad with the up arrow show the correct contents? helper.waitFor(function(){ // Wait for the new position to be in place - return isScrolledIntoView(inner$("div:nth-child(1)"), inner$); // Wait for the DOM to scroll into place + // Wait for the DOM to scroll into place + return isScrolledIntoView(inner$("div:nth-child(1)"), inner$); }).done(function(){ // Once the DOM has registered the items expect(true).to.be(true); done(); @@ -284,17 +298,19 @@ console.log(inner$); */ }); -function prepareDocument(n, target) { // generates a random document with random content on n lines +// generates a random document with random content on n lines +const prepareDocument = (n, target) => { let i = 0; while (i < n) { // for each line target.sendkeys(makeStr()); // generate a random string and send that to the editor target.sendkeys('{enter}'); // generator an enter keypress i++; // rinse n times } -} +}; -function keyEvent(target, charCode, ctrl, shift) { // sends a charCode to the window - const e = target.Event(helper.evtType); +// sends a charCode to the window +const keyEvent = (target, charCode, ctrl, shift) => { + const e = new target.Event(helper.evtType); if (ctrl) { e.ctrlKey = true; // Control key } @@ -304,30 +320,33 @@ function keyEvent(target, charCode, ctrl, shift) { // sends a charCode to the wi e.which = charCode; e.keyCode = charCode; target('#innerdocbody').trigger(e); -} +}; -function makeStr() { // from http://stackoverflow.com/questions/1349404/generate-a-string-of-5-random-characters-in-javascript +// from http://stackoverflow.com/questions/1349404/generate-a-string-of-5-random-characters-in-javascript +const makeStr = () => { let text = ''; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < 5; i++) text += possible.charAt(Math.floor(Math.random() * possible.length)); return text; -} +}; -function isScrolledIntoView(elem, $) { // from http://stackoverflow.com/questions/487073/check-if-element-is-visible-after-scrolling +// from http://stackoverflow.com/questions/487073/check-if-element-is-visible-after-scrolling +const isScrolledIntoView = (elem, $) => { const docViewTop = $(window).scrollTop(); const docViewBottom = docViewTop + $(window).height(); const elemTop = $(elem).offset().top; // how far the element is from the top of it's container - let elemBottom = elemTop + $(elem).height(); // how far plus the height of the elem.. IE is it all in? + // how far plus the height of the elem.. IE is it all in? + let elemBottom = elemTop + $(elem).height(); elemBottom -= 16; // don't ask, sorry but this is needed.. return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop)); -} +}; -function caretPosition($) { +const caretPosition = ($) => { const doc = $.window.document; const pos = doc.getSelection(); pos.y = pos.anchorNode.parentElement.offsetTop; pos.x = pos.anchorNode.parentElement.offsetLeft; return pos; -} +}; diff --git a/tests/frontend/specs/change_user_color.js b/tests/frontend/specs/change_user_color.js index e8c16db37..c8a3bf5b9 100644 --- a/tests/frontend/specs/change_user_color.js +++ b/tests/frontend/specs/change_user_color.js @@ -1,3 +1,5 @@ +'use strict'; + describe('change user color', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -5,7 +7,8 @@ describe('change user color', function () { this.timeout(60000); }); - it('Color picker matches original color and 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(60000); const chrome$ = helper.padChrome$; @@ -60,7 +63,6 @@ describe('change user color', function () { }); it('Own user color is shown when you enter a chat', function (done) { - const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; const $colorOption = helper.padChrome$('#options-colorscheck'); @@ -90,13 +92,15 @@ describe('change user color', function () { $chatButton.click(); const $chatInput = chrome$('#chatinput'); $chatInput.sendkeys('O hi'); // simulate a keypress of typing user - $chatInput.sendkeys('{enter}'); // 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}'); - // check if chat shows up - helper.waitFor(() => chrome$('#chattext').children('p').length !== 0 // wait until the chat message shows up + // wait until the chat message shows up + helper.waitFor(() => chrome$('#chattext').children('p').length !== 0 ).done(() => { const $firstChatMessage = chrome$('#chattext').children('p'); - expect($firstChatMessage.css('background-color')).to.be(testColorRGB); // expect the first chat message to be of the user's color + // expect the first chat message to be of the user's color + expect($firstChatMessage.css('background-color')).to.be(testColorRGB); done(); }); }); diff --git a/tests/frontend/specs/change_user_name.js b/tests/frontend/specs/change_user_name.js index 0b9132f80..3c4b8b5bc 100644 --- a/tests/frontend/specs/change_user_name.js +++ b/tests/frontend/specs/change_user_name.js @@ -1,3 +1,5 @@ +'use strict'; + describe('change username value', function () { // create a new pad before each test run beforeEach(function (cb) { diff --git a/tests/frontend/specs/chat.js b/tests/frontend/specs/chat.js index d45988d60..fbc6ce788 100644 --- a/tests/frontend/specs/chat.js +++ b/tests/frontend/specs/chat.js @@ -1,10 +1,13 @@ +'use strict'; + describe('Chat messages and UI', function () { // create a new pad before each test run beforeEach(function (cb) { helper.newPad(cb); }); - it('opens chat, sends a message, makes sure it exists on the page and hides chat', async function () { + it('opens chat, sends a message, makes sure it exists ' + + 'on the page and hides chat', async function () { const chatValue = 'JohnMcLear'; await helper.showChat(); @@ -31,7 +34,8 @@ describe('Chat messages and UI', function () { await helper.showChat(); - await helper.sendChatMessage(`{enter}${chatValue}{enter}`); // simulate a keypress of typing enter, mluto and enter (to send 'mluto') + // simulate a keypress of typing enter, mluto and enter (to send 'mluto') + await helper.sendChatMessage(`{enter}${chatValue}{enter}`); const chat = helper.chatTextParagraphs(); @@ -44,7 +48,8 @@ describe('Chat messages and UI', function () { expect(chat.text()).to.be(`${username}${time} ${chatValue}`); }); - it('makes chat stick to right side of the screen via settings, remove sticky via settings, close it', async function () { + it('makes chat stick to right side of the screen via settings, ' + + 'remove sticky via settings, close it', async function () { await helper.showSettings(); await helper.enableStickyChatviaSettings(); @@ -60,7 +65,8 @@ describe('Chat messages and UI', function () { expect(helper.isChatboxShown()).to.be(false); }); - it('makes chat stick to right side of the screen via icon on the top right, remove sticky via icon, close it', async function () { + it('makes chat stick to right side of the screen via icon on the top' + + ' right, remove sticky via icon, close it', async function () { await helper.showChat(); await helper.enableStickyChatviaIcon(); @@ -76,10 +82,9 @@ describe('Chat messages and UI', function () { expect(helper.isChatboxShown()).to.be(false); }); - xit('Checks showChat=false URL Parameter hides chat then when removed it shows chat', function (done) { + xit('Checks showChat=false URL Parameter hides chat then' + + ' when removed it shows chat', function (done) { this.timeout(60000); - const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; setTimeout(() => { // give it a second to save the username on the server side helper.newPad({ // get a new pad, but don't clear the cookies diff --git a/tests/frontend/specs/chat_load_messages.js b/tests/frontend/specs/chat_load_messages.js index 29c1734ca..63d90fd63 100644 --- a/tests/frontend/specs/chat_load_messages.js +++ b/tests/frontend/specs/chat_load_messages.js @@ -1,3 +1,5 @@ +'use strict'; + describe('chat-load-messages', function () { let padName; @@ -7,7 +9,6 @@ describe('chat-load-messages', function () { }); it('adds a lot of messages', function (done) { - const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; const chatButton = chrome$('#chaticon'); chatButton.click(); @@ -19,12 +20,12 @@ describe('chat-load-messages', function () { const messages = 140; for (let i = 1; i <= messages; i++) { let num = `${i}`; - if (num.length == 1) num = `00${num}`; - if (num.length == 2) num = `0${num}`; + if (num.length === 1) num = `00${num}`; + if (num.length === 2) num = `0${num}`; chatInput.sendkeys(`msg${num}`); chatInput.sendkeys('{enter}'); } - helper.waitFor(() => chatText.children('p').length == messages, 60000).always(() => { + helper.waitFor(() => chatText.children('p').length === messages, 60000).always(() => { expect(chatText.children('p').length).to.be(messages); helper.newPad(done, padName); }); @@ -38,7 +39,7 @@ describe('chat-load-messages', function () { const chatButton = chrome$('#chaticon'); chatButton.click(); chatText = chrome$('#chattext'); - return chatText.children('p').length == expectedCount; + return chatText.children('p').length === expectedCount; }).always(() => { expect(chatText.children('p').length).to.be(expectedCount); done(); @@ -54,7 +55,7 @@ describe('chat-load-messages', function () { const loadMsgBtn = chrome$('#chatloadmessagesbutton'); loadMsgBtn.click(); - helper.waitFor(() => chatText.children('p').length == expectedCount).always(() => { + helper.waitFor(() => chatText.children('p').length === expectedCount).always(() => { expect(chatText.children('p').length).to.be(expectedCount); done(); }); @@ -65,13 +66,12 @@ describe('chat-load-messages', function () { const chrome$ = helper.padChrome$; const chatButton = chrome$('#chaticon'); chatButton.click(); - const chatText = chrome$('#chattext'); const loadMsgBtn = chrome$('#chatloadmessagesbutton'); const loadMsgBall = chrome$('#chatloadmessagesball'); loadMsgBtn.click(); - helper.waitFor(() => loadMsgBtn.css('display') == expectedDisplay && - loadMsgBall.css('display') == expectedDisplay).always(() => { + helper.waitFor(() => loadMsgBtn.css('display') === expectedDisplay && + loadMsgBall.css('display') === expectedDisplay).always(() => { expect(loadMsgBtn.css('display')).to.be(expectedDisplay); expect(loadMsgBall.css('display')).to.be(expectedDisplay); done(); diff --git a/tests/frontend/specs/clear_authorship_colors.js b/tests/frontend/specs/clear_authorship_colors.js index f622e912a..63d4c2f54 100644 --- a/tests/frontend/specs/clear_authorship_colors.js +++ b/tests/frontend/specs/clear_authorship_colors.js @@ -1,3 +1,5 @@ +'use strict'; + describe('clear authorship colors button', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -17,9 +19,6 @@ describe('clear authorship colors button', function () { // get the first text element out of the inner iframe const $firstTextElement = inner$('div').first(); - // Get the original text - const originalText = inner$('div').first().text(); - // Set some new text const sentText = 'Hello'; @@ -28,7 +27,8 @@ describe('clear authorship colors button', function () { $firstTextElement.sendkeys(sentText); $firstTextElement.sendkeys('{rightarrow}'); - helper.waitFor(() => inner$('div span').first().attr('class').indexOf('author') !== -1 // wait until we have the full value available + // wait until we have the full value available + helper.waitFor(() => inner$('div span').first().attr('class').indexOf('author') !== -1 ).done(() => { // IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship inner$('div').first().focus(); @@ -37,16 +37,13 @@ describe('clear authorship colors button', function () { const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship'); $clearauthorshipcolorsButton.click(); - // does the first divs span include an author class? - var hasAuthorClass = inner$('div span').first().attr('class').indexOf('author') !== -1; - // expect(hasAuthorClass).to.be(false); - // does the first div include an author class? - var hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; + const hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; expect(hasAuthorClass).to.be(false); helper.waitFor(() => { - const disconnectVisible = chrome$('div.disconnected').attr('class').indexOf('visible') === -1; + const disconnectVisible = + chrome$('div.disconnected').attr('class').indexOf('visible') === -1; return (disconnectVisible === true); }); @@ -69,9 +66,6 @@ describe('clear authorship colors button', function () { // get the first text element out of the inner iframe const $firstTextElement = inner$('div').first(); - // Get the original text - const originalText = inner$('div').first().text(); - // Set some new text const sentText = 'Hello'; @@ -80,7 +74,9 @@ describe('clear authorship colors button', function () { $firstTextElement.sendkeys(sentText); $firstTextElement.sendkeys('{rightarrow}'); - helper.waitFor(() => inner$('div span').first().attr('class').indexOf('author') !== -1 // wait until we have the full value available + // wait until we have the full value available + helper.waitFor( + () => inner$('div span').first().attr('class').indexOf('author') !== -1 ).done(() => { // IE hates you if you don't give focus to the inner frame bevore you do a clearAuthorship inner$('div').first().focus(); @@ -89,15 +85,11 @@ describe('clear authorship colors button', function () { const $clearauthorshipcolorsButton = chrome$('.buttonicon-clearauthorship'); $clearauthorshipcolorsButton.click(); - // does the first divs span include an author class? - var hasAuthorClass = inner$('div span').first().attr('class').indexOf('author') !== -1; - // expect(hasAuthorClass).to.be(false); - // does the first div include an author class? - var hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; + let hasAuthorClass = inner$('div').first().attr('class').indexOf('author') !== -1; expect(hasAuthorClass).to.be(false); - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 90; // z inner$('#innerdocbody').trigger(e); // shouldn't od anything @@ -115,7 +107,8 @@ describe('clear authorship colors button', function () { expect(hasAuthorClass).to.be(false); helper.waitFor(() => { - const disconnectVisible = chrome$('div.disconnected').attr('class').indexOf('visible') === -1; + const disconnectVisible = + chrome$('div.disconnected').attr('class').indexOf('visible') === -1; return (disconnectVisible === true); }); diff --git a/tests/frontend/specs/delete.js b/tests/frontend/specs/delete.js index 4267aeec7..6cde43f47 100644 --- a/tests/frontend/specs/delete.js +++ b/tests/frontend/specs/delete.js @@ -1,3 +1,5 @@ +'use strict'; + describe('delete keystroke', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -7,7 +9,6 @@ describe('delete keystroke', function () { it('makes text delete', function (done) { const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // get the first text element out of the inner iframe const $firstTextElement = inner$('div').first(); @@ -15,15 +16,10 @@ describe('delete keystroke', function () { // get the original length of this element const elementLength = $firstTextElement.text().length; - // get the original string value minus the last char - const originalTextValue = $firstTextElement.text(); - const originalTextValueMinusFirstChar = originalTextValue.substring(1, originalTextValue.length); - // simulate key presses to delete content $firstTextElement.sendkeys('{leftarrow}'); // simulate a keypress of the left arrow key $firstTextElement.sendkeys('{del}'); // simulate a keypress of delete - // ace creates a new dom element when you press a keystroke, so just get the first text element again const $newFirstTextElement = inner$('div').first(); // get the new length of this element diff --git a/tests/frontend/specs/drag_and_drop.js b/tests/frontend/specs/drag_and_drop.js index a9726111c..2d1339beb 100644 --- a/tests/frontend/specs/drag_and_drop.js +++ b/tests/frontend/specs/drag_and_drop.js @@ -1,4 +1,6 @@ -// WARNING: drag and drop is only simulated on these tests, so manual testing might also be necessary +'use strict'; + +// WARNING: drag and drop is only simulated on these tests, manual testing might also be necessary describe('drag and drop', function () { before(function (done) { helper.newPad(() => { @@ -78,18 +80,19 @@ describe('drag and drop', function () { }); /* ********************* Helper functions/constants ********************* */ - var TARGET_LINE = 2; - var FIRST_SOURCE_LINE = 5; + const TARGET_LINE = 2; + const FIRST_SOURCE_LINE = 5; - var getLine = function (lineNumber) { + const getLine = (lineNumber) => { const $lines = helper.padInner$('div'); return $lines.slice(lineNumber, lineNumber + 1); }; - var createScriptWithSeveralLines = function (done) { + const createScriptWithSeveralLines = (done) => { // create some lines to be used on the tests const $firstLine = helper.padInner$('div').first(); - $firstLine.html('...
...
Target line []
...
...
Source line 1.
Source line 2.
'); + $firstLine.html('...
...
Target line []
...
...
' + + 'Source line 1.
Source line 2.
'); // wait for lines to be split helper.waitFor(() => { @@ -98,7 +101,7 @@ describe('drag and drop', function () { }).done(done); }; - var selectPartOfSourceLine = function () { + const selectPartOfSourceLine = () => { const $sourceLine = getLine(FIRST_SOURCE_LINE); // select 'line 1' from 'Source line 1.' @@ -106,14 +109,14 @@ describe('drag and drop', function () { const end = start + 'line 1'.length; helper.selectLines($sourceLine, $sourceLine, start, end); }; - var selectMultipleSourceLines = function () { + const selectMultipleSourceLines = () => { const $firstSourceLine = getLine(FIRST_SOURCE_LINE); const $lastSourceLine = getLine(FIRST_SOURCE_LINE + 1); helper.selectLines($firstSourceLine, $lastSourceLine); }; - var dragSelectedTextAndDropItIntoMiddleOfLine = function (targetLineNumber) { + const dragSelectedTextAndDropItIntoMiddleOfLine = (targetLineNumber) => { // dragstart: start dragging content triggerEvent('dragstart'); @@ -126,7 +129,7 @@ describe('drag and drop', function () { triggerEvent('dragend'); }; - var getHtmlFromSelectedText = function () { + const getHtmlFromSelectedText = () => { const innerDocument = helper.padInner$.document; const range = innerDocument.getSelection().getRangeAt(0); @@ -139,12 +142,12 @@ describe('drag and drop', function () { return draggedHtml; }; - var triggerEvent = function (eventName) { - const event = helper.padInner$.Event(eventName); + const triggerEvent = (eventName) => { + const event = new helper.padInner$.Event(eventName); helper.padInner$('#innerdocbody').trigger(event); }; - var moveSelectionIntoTarget = function (draggedHtml, targetLineNumber) { + const moveSelectionIntoTarget = (draggedHtml, targetLineNumber) => { const innerDocument = helper.padInner$.document; // delete original content diff --git a/tests/frontend/specs/embed_value.js b/tests/frontend/specs/embed_value.js index d6fb8c977..dac4c869d 100644 --- a/tests/frontend/specs/embed_value.js +++ b/tests/frontend/specs/embed_value.js @@ -1,3 +1,5 @@ +'use strict'; + describe('embed links', function () { const objectify = function (str) { const hash = {}; diff --git a/tests/frontend/specs/enter.js b/tests/frontend/specs/enter.js index 6108d7f82..2f5a90a69 100644 --- a/tests/frontend/specs/enter.js +++ b/tests/frontend/specs/enter.js @@ -1,3 +1,5 @@ +'use strict'; + describe('enter keystroke', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -7,7 +9,6 @@ describe('enter keystroke', function () { it('creates a new line & puts cursor onto a new line', function (done) { const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // get the first text element out of the inner iframe const $firstTextElement = inner$('div').first(); @@ -18,14 +19,12 @@ describe('enter keystroke', function () { // simulate key presses to enter content $firstTextElement.sendkeys('{enter}'); - // ace creates a new dom element when you press a keystroke, so just get the first text element again - const $newFirstTextElement = inner$('div').first(); - helper.waitFor(() => inner$('div').first().text() === '').done(() => { const $newSecondLine = inner$('div').first().next(); const newFirstTextElementValue = inner$('div').first().text(); expect(newFirstTextElementValue).to.be(''); // expect the first line to be blank - expect($newSecondLine.text()).to.be(originalTextValue); // expect the second line to be the same as the original first line. + // expect the second line to be the same as the original first line. + expect($newSecondLine.text()).to.be(originalTextValue); done(); }); }); diff --git a/tests/frontend/specs/font_type.js b/tests/frontend/specs/font_type.js index 51971da39..68df2f5e7 100644 --- a/tests/frontend/specs/font_type.js +++ b/tests/frontend/specs/font_type.js @@ -1,3 +1,5 @@ +'use strict'; + describe('font select', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -15,7 +17,6 @@ describe('font select', function () { // get the font menu and RobotoMono option const $viewfontmenu = chrome$('#viewfontmenu'); - const $RobotoMonooption = $viewfontmenu.find('[value=RobotoMono]'); // select RobotoMono and fire change event // $RobotoMonooption.attr('selected','selected'); diff --git a/tests/frontend/specs/helper.js b/tests/frontend/specs/helper.js index 6bc6a3643..cb72fcf7a 100644 --- a/tests/frontend/specs/helper.js +++ b/tests/frontend/specs/helper.js @@ -1,3 +1,5 @@ +'use strict'; + describe('the test helper', function () { describe('the newPad method', function () { xit("doesn't leak memory if you creates iframes over and over again", function (done) { @@ -5,7 +7,7 @@ describe('the test helper', function () { let times = 10; - var loadPad = function () { + const loadPad = () => { helper.newPad(() => { times--; if (times > 0) { @@ -75,13 +77,14 @@ describe('the test helper', function () { // Before refreshing, make sure the name is there expect($usernameInput.val()).to.be('John McLear'); - // Now that we have a chrome, we can set a pad cookie, so we can confirm it gets wiped as well + // Now that we have a chrome, we can set a pad cookie + // so we can confirm it gets wiped as well chrome$.document.cookie = 'prefsHtml=baz;expires=Thu, 01 Jan 3030 00:00:00 GMT'; expect(chrome$.document.cookie).to.contain('prefsHtml=baz'); - // Cookies are weird. Because it's attached to chrome$ (as helper.setPadCookies does), AND we - // didn't put path=/, we shouldn't expect it to be visible on window.document.cookie. Let's just - // be sure. + // Cookies are weird. Because it's attached to chrome$ (as helper.setPadCookies does) + // AND we didn't put path=/, we shouldn't expect it to be visible on + // window.document.cookie. Let's just be sure. expect(window.document.cookie).to.not.contain('prefsHtml=baz'); setTimeout(() => { // give it a second to save the username on the server side @@ -266,7 +269,8 @@ describe('the test helper', function () { this.timeout(60000); }); - it('changes editor selection to be between startOffset of $startLine and endOffset of $endLine', function (done) { + it('changes editor selection to be between startOffset of $startLine ' + + 'and endOffset of $endLine', function (done) { const inner$ = helper.padInner$; const startOffset = 2; @@ -313,7 +317,8 @@ describe('the test helper', function () { * is not consistent between browsers but that's the situation so that's * how I'm covering it in this test. */ - expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines to test'); + expect(cleanText( + selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines to test'); done(); }); @@ -365,12 +370,14 @@ describe('the test helper', function () { * is not consistent between browsers but that's the situation so that's * how I'm covering it in this test. */ - expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines to test'); + expect(cleanText( + selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('ort lines to test'); done(); }); - it('selects all text between beginning of $startLine and end of $endLine when no offset is provided', function (done) { + it('selects all text between beginning of $startLine and end of $endLine ' + + 'when no offset is provided', function (done) { const inner$ = helper.padInner$; const $lines = inner$('div'); @@ -388,7 +395,8 @@ describe('the test helper', function () { * is not consistent between browsers but that's the situation so that's * how I'm covering it in this test. */ - expect(cleanText(selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('short lines to test'); + expect(cleanText( + selection.toString().replace(/(\r\n|\n|\r)/gm, ''))).to.be('short lines to test'); done(); }); diff --git a/tests/frontend/specs/importexport.js b/tests/frontend/specs/importexport.js index 0be2a0744..4eb95eeb0 100644 --- a/tests/frontend/specs/importexport.js +++ b/tests/frontend/specs/importexport.js @@ -1,3 +1,5 @@ +'use strict'; + describe('import functionality', function () { beforeEach(function (cb) { helper.newPad(cb); // creates a new pad @@ -16,7 +18,6 @@ describe('import functionality', function () { return newtext; } function importrequest(data, importurl, type) { - let success; let error; const result = $.ajax({ url: importurl, @@ -27,7 +28,17 @@ describe('import functionality', function () { accepts: { text: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', }, - data: `Content-Type: multipart/form-data; boundary=--boundary\r\n\r\n--boundary\r\nContent-Disposition: form-data; name="file"; filename="import.${type}"\r\nContent-Type: text/plain\r\n\r\n${data}\r\n\r\n--boundary`, + data: [ + 'Content-Type: multipart/form-data; boundary=--boundary', + '', + '--boundary', + `Content-Disposition: form-data; name="file"; filename="import.${type}"`, + 'Content-Type: text/plain', + '', + data, + '', + '--boundary', + ].join('\r\n'), error(res) { error = res; }, @@ -56,7 +67,8 @@ describe('import functionality', function () { const importurl = `${helper.padChrome$.window.location.href}/import`; const textWithNewLines = 'imported text\nnewline'; importrequest(textWithNewLines, importurl, 'txt'); - helper.waitFor(() => expect(getinnertext()).to.be('imported text\nnewline\n
\n')); + helper.waitFor(() => expect(getinnertext()) + .to.be('imported text\nnewline\n
\n')); const results = exportfunc(helper.padChrome$.window.location.href); expect(results[0][1]).to.be('imported text
newline

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

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

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

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

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

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

  • bullet line 2
    • bullet2 line 1

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

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

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

  • bullet line 2
    • bullet2 line 1

    • bullet2 line 2

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

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

    • bullet2 line 2

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

  • bullet line 2
    • bullet2 line 1

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

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

  • bullet line 2
    • bullet2 line 1

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

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

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

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

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

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

  • bullet line 2
    • bullet2 line 1

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

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

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

      • bullet line 2
        • bullet2 line 1

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

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

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

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

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

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

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

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

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

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

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

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

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

      • indent 1 line 2
        • indent 2 times line 1

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

      • indent 1 line 2
        • indent 2 times line 1

        • indent 2 times line 2

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

      • indent line 2
        • indent2 line 1

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

        • indent line 2
          • indent2 line 1

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

        '); + /* eslint-disable-next-line max-len */ expect(results[1][1]).to.be('\tindent line 1\n\n\tindent line 2\n\t\tindent2 line 1\n\n\t\t\t\tindent4 line 2 bisu\n\t\t\t\tindent4 line 2 bs\n\t\t\t\tindent4 line 2 uuis\n\t\t\t\t\t\t\t\tfoo\n\t\t\t\t\t\t\t\tfoobar bs\n\t\t\t\t\tfoobar\n\n'); done(); }); diff --git a/tests/frontend/specs/indentation.js b/tests/frontend/specs/indentation.js index c52f5f406..006c52bb4 100644 --- a/tests/frontend/specs/indentation.js +++ b/tests/frontend/specs/indentation.js @@ -1,3 +1,5 @@ +'use strict'; + describe('indentation button', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -7,7 +9,6 @@ describe('indentation button', function () { it('indent text with keypress', function (done) { const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // get the first text element out of the inner iframe const $firstTextElement = inner$('div').first(); @@ -15,7 +16,7 @@ describe('indentation button', function () { // select this text element $firstTextElement.sendkeys('{selectall}'); - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.keyCode = 9; // tab :| inner$('#innerdocbody').trigger(e); @@ -56,9 +57,9 @@ 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) { const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // type a bit, make a line break and type again const $firstTextElement = inner$('div').first(); @@ -77,7 +78,8 @@ describe('indentation button', function () { // curly braces const $lineWithCurlyBraces = inner$('div').first().next().next().next(); $lineWithCurlyBraces.sendkeys('{{}'); - pressEnter(); // cannot use sendkeys('{enter}') here, browser does not read the command properly + // cannot use sendkeys('{enter}') here, browser does not read the command properly + pressEnter(); const $lineAfterCurlyBraces = inner$('div').first().next().next().next().next(); expect($lineAfterCurlyBraces.text()).to.match(/\s{4}/); // tab === 4 spaces @@ -106,9 +108,9 @@ 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) { const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // type a bit, make a line break and type again const $firstTextElement = inner$('div').first(); @@ -124,13 +126,15 @@ describe('indentation button', function () { $lineWithColon.sendkeys(':'); pressEnter(); const $lineAfterColon = inner$('div').first().next(); - expect($lineAfterColon.text()).to.match(/\s{6}/); // previous line indentation + regular tab (4 spaces) + // previous line indentation + regular tab (4 spaces) + expect($lineAfterColon.text()).to.match(/\s{6}/); done(); }); }); - 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 () { const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -182,7 +186,6 @@ describe('indentation button', function () { var $indentButton = testHelper.$getPadChrome().find(".buttonicon-indent"); $indentButton.click(); - //ace creates a new dom element when you press a button, so just get the first text element again var newFirstTextElement = $inner.find("div").first(); // is there a list-indent class element now? @@ -220,7 +223,6 @@ describe('indentation button', function () { $outdentButton.click(); $outdentButton.click(); - //ace creates a new dom element when you press a button, so just get the first text element again var newFirstTextElement = $inner.find("div").first(); // is there a list-indent class element now? @@ -267,7 +269,9 @@ describe('indentation button', function () { //get the second text element out of the inner iframe setTimeout(function(){ // THIS IS REALLY BAD - var secondTextElement = $('iframe').contents().find('iframe').contents().find('iframe').contents().find('body > div').get(1); // THIS IS UGLY + var secondTextElement = $('iframe').contents() + .find('iframe').contents() + .find('iframe').contents().find('body > div').get(1); // THIS IS UGLY // is there a list-indent class element now? var firstChild = secondTextElement.children(":first"); @@ -282,7 +286,10 @@ describe('indentation button', function () { expect(isLI).to.be(true); //get the first text element out of the inner iframe - var thirdTextElement = $('iframe').contents().find('iframe').contents().find('iframe').contents().find('body > div').get(2); // THIS IS UGLY TOO + var thirdTextElement = $('iframe').contents() + .find('iframe').contents() + .find('iframe').contents() + .find('body > div').get(2); // THIS IS UGLY TOO // is there a list-indent class element now? var firstChild = thirdTextElement.children(":first"); @@ -300,9 +307,9 @@ describe('indentation button', function () { });*/ }); -function pressEnter() { +const pressEnter = () => { const inner$ = helper.padInner$; - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.keyCode = 13; // enter :| inner$('#innerdocbody').trigger(e); -} +}; diff --git a/tests/frontend/specs/italic.js b/tests/frontend/specs/italic.js index 3660f71f3..cbaf9e3da 100644 --- a/tests/frontend/specs/italic.js +++ b/tests/frontend/specs/italic.js @@ -1,3 +1,5 @@ +'use strict'; + describe('italic some text', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -19,7 +21,7 @@ describe('italic some text', function () { const $boldButton = chrome$('.buttonicon-italic'); $boldButton.click(); - // ace creates a new dom element when you press a button, so just get the first text element again + // ace creates a new dom element when you press a button, just get the first text element again const $newFirstTextElement = inner$('div').first(); // is there a element now? @@ -36,7 +38,6 @@ describe('italic some text', function () { it('makes text italic using keypress', function (done) { const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // get the first text element out of the inner iframe const $firstTextElement = inner$('div').first(); @@ -44,12 +45,12 @@ describe('italic some text', function () { // select this text element $firstTextElement.sendkeys('{selectall}'); - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 105; // i inner$('#innerdocbody').trigger(e); - // ace creates a new dom element when you press a button, so just get the first text element again + // ace creates a new dom element when you press a button, just get the first text element again const $newFirstTextElement = inner$('div').first(); // is there a element now? diff --git a/tests/frontend/specs/language.js b/tests/frontend/specs/language.js index d29b2407e..072c64e92 100644 --- a/tests/frontend/specs/language.js +++ b/tests/frontend/specs/language.js @@ -1,6 +1,8 @@ -function deletecookie(name) { +'use strict'; + +const deletecookie = (name) => { document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:01 GMT;`; -} +}; describe('Language select and change', function () { // Destroy language cookies @@ -14,7 +16,6 @@ describe('Language select and change', function () { // Destroy language cookies it('makes text german', function (done) { - const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; // click on the settings button to make settings visible @@ -29,7 +30,7 @@ describe('Language select and change', function () { $languageoption.attr('selected', 'selected'); $language.change(); - helper.waitFor(() => chrome$('.buttonicon-bold').parent()[0].title == 'Fett (Strg-B)') + helper.waitFor(() => chrome$('.buttonicon-bold').parent()[0].title === 'Fett (Strg-B)') .done(() => { // get the value of the bold button const $boldButton = chrome$('.buttonicon-bold').parent(); @@ -44,7 +45,6 @@ describe('Language select and change', function () { }); it('makes text English', function (done) { - const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; // click on the settings button to make settings visible @@ -60,7 +60,7 @@ describe('Language select and change', function () { // get the value of the bold button const $boldButton = chrome$('.buttonicon-bold').parent(); - helper.waitFor(() => $boldButton[0].title != 'Fett (Strg+B)') + helper.waitFor(() => $boldButton[0].title !== 'Fett (Strg+B)') .done(() => { // get the value of the bold button const $boldButton = chrome$('.buttonicon-bold').parent(); @@ -75,7 +75,6 @@ describe('Language select and change', function () { }); it('changes direction when picking an rtl lang', function (done) { - const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; // click on the settings button to make settings visible @@ -91,7 +90,7 @@ describe('Language select and change', function () { $language.val('ar'); $languageoption.change(); - helper.waitFor(() => chrome$('html')[0].dir != 'ltr') + helper.waitFor(() => chrome$('html')[0].dir !== 'ltr') .done(() => { // check if the document's direction was changed expect(chrome$('html')[0].dir).to.be('rtl'); @@ -100,7 +99,6 @@ describe('Language select and change', function () { }); it('changes direction when picking an ltr lang', function (done) { - const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; // click on the settings button to make settings visible @@ -117,7 +115,7 @@ describe('Language select and change', function () { $language.val('en'); $languageoption.change(); - helper.waitFor(() => chrome$('html')[0].dir != 'rtl') + helper.waitFor(() => chrome$('html')[0].dir !== 'rtl') .done(() => { // check if the document's direction was changed expect(chrome$('html')[0].dir).to.be('ltr'); diff --git a/tests/frontend/specs/multiple_authors_clear_authorship_colors.js b/tests/frontend/specs/multiple_authors_clear_authorship_colors.js index f532ea4be..19dfb44f9 100755 --- a/tests/frontend/specs/multiple_authors_clear_authorship_colors.js +++ b/tests/frontend/specs/multiple_authors_clear_authorship_colors.js @@ -1,7 +1,9 @@ +'use strict'; + describe('author of pad edition', function () { // author 1 creates a new pad with some content (regular lines and lists) before(function (done) { - var padId = helper.newPad(() => { + const padId = helper.newPad(() => { // make sure pad has at least 3 lines const $firstLine = helper.padInner$('div').first(); $firstLine.html('Hello World'); @@ -13,7 +15,8 @@ describe('author of pad edition', function () { setTimeout(() => { // Expire cookie, so author is changed after reloading the pad. // See https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#Example_4_Reset_the_previous_cookie - 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); }, 1000); @@ -27,7 +30,7 @@ describe('author of pad edition', function () { clearAuthorship(done); }); - var clearAuthorship = function (done) { + const clearAuthorship = (done) => { const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; diff --git a/tests/frontend/specs/ordered_list.js b/tests/frontend/specs/ordered_list.js index a932335e8..d069a6487 100644 --- a/tests/frontend/specs/ordered_list.js +++ b/tests/frontend/specs/ordered_list.js @@ -1,3 +1,5 @@ +'use strict'; + describe('assign ordered list', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -34,7 +36,8 @@ describe('assign ordered list', function () { }); it('does not insert unordered list', function (done) { - helper.waitFor(() => helper.padInner$('div').first().find('ol li').length === 1).done(() => { + helper.waitFor( + () => helper.padInner$('div').first().find('ol li').length === 1).done(() => { expect().fail(() => 'Unordered list inserted, should ignore shortcut'); }).fail(() => { done(); @@ -62,7 +65,8 @@ describe('assign ordered list', function () { }); it('does not insert unordered list', function (done) { - helper.waitFor(() => helper.padInner$('div').first().find('ol li').length === 1).done(() => { + helper.waitFor( + () => helper.padInner$('div').first().find('ol li').length === 1).done(() => { expect().fail(() => 'Unordered list inserted, should ignore shortcut'); }).fail(() => { done(); @@ -71,7 +75,8 @@ describe('assign ordered list', function () { }); }); - xit('issue #1125 keeps the numbered list on enter for the new line - EMULATES PASTING INTO A PAD', function (done) { + xit('issue #1125 keeps the numbered list on enter for the new line', function (done) { + // EMULATES PASTING INTO A PAD const inner$ = helper.padInner$; const chrome$ = helper.padChrome$; @@ -91,24 +96,25 @@ describe('assign ordered list', function () { expect(hasOLElement).to.be(true); expect($newSecondLine.text()).to.be('line 2'); const hasLineNumber = $newSecondLine.find('ol').attr('start') === 2; - expect(hasLineNumber).to.be(true); // This doesn't work because pasting in content doesn't work + // This doesn't work because pasting in content doesn't work + expect(hasLineNumber).to.be(true); done(); }); }); - var triggerCtrlShiftShortcut = function (shortcutChar) { + const triggerCtrlShiftShortcut = (shortcutChar) => { const inner$ = helper.padInner$; - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.ctrlKey = true; e.shiftKey = true; e.which = shortcutChar.toString().charCodeAt(0); inner$('#innerdocbody').trigger(e); }; - var makeSureShortcutIsDisabled = function (shortcut) { + const makeSureShortcutIsDisabled = (shortcut) => { helper.padChrome$.window.clientVars.padShortcutEnabled[shortcut] = false; }; - var makeSureShortcutIsEnabled = function (shortcut) { + const makeSureShortcutIsEnabled = (shortcut) => { helper.padChrome$.window.clientVars.padShortcutEnabled[shortcut] = true; }; }); @@ -133,7 +139,7 @@ describe('Pressing Tab in an OL increases and decreases indentation', function ( const $insertorderedlistButton = chrome$('.buttonicon-insertorderedlist'); $insertorderedlistButton.click(); - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.keyCode = 9; // tab inner$('#innerdocbody').trigger(e); @@ -147,7 +153,8 @@ describe('Pressing Tab in an OL increases and decreases indentation', function ( }); -describe('Pressing indent/outdent button in an OL increases and decreases indentation and bullet / ol formatting', function () { +describe('Pressing indent/outdent button in an OL increases and ' + + 'decreases indentation and bullet / ol formatting', function () { // create a new pad before each test run beforeEach(function (cb) { helper.newPad(cb); diff --git a/tests/frontend/specs/pad_modal.js b/tests/frontend/specs/pad_modal.js index 1711e38b8..30277d5de 100644 --- a/tests/frontend/specs/pad_modal.js +++ b/tests/frontend/specs/pad_modal.js @@ -1,3 +1,5 @@ +'use strict'; + describe('Pad modal', function () { context('when modal is a "force reconnect" message', function () { const MODAL_SELECTOR = '#connectivity'; @@ -93,17 +95,17 @@ describe('Pad modal', function () { }); }); - var clickOnPadInner = function () { + const clickOnPadInner = () => { const $editor = helper.padInner$('#innerdocbody'); $editor.click(); }; - var clickOnPadOuter = function () { + const clickOnPadOuter = () => { const $lineNumbersColumn = helper.padOuter$('#sidedivinner'); $lineNumbersColumn.click(); }; - var openSettingsAndWaitForModalToBeVisible = function (done) { + const openSettingsAndWaitForModalToBeVisible = (done) => { helper.padChrome$('.buttonicon-settings').click(); // wait for modal to be displayed @@ -111,7 +113,7 @@ describe('Pad modal', function () { helper.waitFor(() => isModalOpened(modalSelector), 10000).done(done); }; - var isEditorDisabled = function () { + const isEditorDisabled = () => { const editorDocument = helper.padOuter$("iframe[name='ace_inner']").get(0).contentDocument; const editorBody = editorDocument.getElementById('innerdocbody'); @@ -121,7 +123,7 @@ describe('Pad modal', function () { return editorIsDisabled; }; - var isModalOpened = function (modalSelector) { + const isModalOpened = (modalSelector) => { const $modal = helper.padChrome$(modalSelector); return $modal.hasClass('popup-show'); diff --git a/tests/frontend/specs/redo.js b/tests/frontend/specs/redo.js index 58d5b6c12..3e8d3a168 100644 --- a/tests/frontend/specs/redo.js +++ b/tests/frontend/specs/redo.js @@ -1,3 +1,5 @@ +'use strict'; + describe('undo button then redo button', function () { beforeEach(function (cb) { helper.newPad(cb); // creates a new pad @@ -33,7 +35,6 @@ describe('undo button then redo button', function () { it('redo some typing with keypress', function (done) { const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // get the first text element inside the editable space const $firstTextElement = inner$('div span').first(); @@ -44,12 +45,12 @@ describe('undo button then redo button', function () { const modifiedValue = $firstTextElement.text(); // get the modified value expect(modifiedValue).not.to.be(originalValue); // expect the value to change - var e = inner$.Event(helper.evtType); + let e = inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 90; // z inner$('#innerdocbody').trigger(e); - var e = inner$.Event(helper.evtType); + e = inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 121; // y inner$('#innerdocbody').trigger(e); diff --git a/tests/frontend/specs/responsiveness.js b/tests/frontend/specs/responsiveness.js index 63803f641..ec63faa10 100644 --- a/tests/frontend/specs/responsiveness.js +++ b/tests/frontend/specs/responsiveness.js @@ -1,14 +1,19 @@ +'use strict'; + // Test for https://github.com/ether/etherpad-lite/issues/1763 // This test fails in Opera, IE and Safari -// Opera fails due to a weird way of handling the order of execution, yet actual performance seems fine +// Opera fails due to a weird way of handling the order of execution, +// yet actual performance seems fine // Safari fails due the delay being too great yet the actual performance seems fine // Firefox might panic that the script is taking too long so will fail // IE will fail due to running out of memory as it can't fit 2M chars in memory. -// Just FYI Google Docs crashes on large docs whilst trying to Save, it's likely the limitations we are +// Just FYI Google Docs crashes on large docs whilst trying to Save, +// it's likely the limitations we are // experiencing are more to do with browser limitations than improper implementation. -// A ueber fix for this would be to have a separate lower cpu priority thread that handles operations that aren't +// A ueber fix for this would be to have a separate lower cpu priority +// thread that handles operations that aren't // visible to the user. // Adapted from John McLear's original test case. @@ -20,16 +25,18 @@ xdescribe('Responsiveness of Editor', function () { this.timeout(6000); }); // JM commented out on 8th Sep 2020 for a release, after release this needs uncommenting - // And the test needs to be fixed to work in Firefox 52 on Windows 7. I am not sure why it fails on this specific platform - // The errors show this.timeout... then crash the browser but I am sure something is actually causing the stack trace and + // And the test needs to be fixed to work in Firefox 52 on Windows 7. + // I am not sure why it fails on this specific platform + // The errors show this.timeout... then crash the browser but + // I am sure something is actually causing the stack trace and // I just need to narrow down what, offers to help accepted. it('Fast response to keypress in pad with large amount of contents', function (done) { // skip on Windows Firefox 52.0 - if (window.bowser && window.bowser.windows && window.bowser.firefox && window.bowser.version == '52.0') { + if (window.bowser && + window.bowser.windows && window.bowser.firefox && window.bowser.version === '52.0') { this.skip(); } const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; const chars = '0000000000'; // row of placeholder chars const amount = 200000; // number of blocks of chars we will insert const length = (amount * (chars.length) + 1); // include a counter for each space @@ -39,7 +46,7 @@ xdescribe('Responsiveness of Editor', function () { // get keys to send const keyMultiplier = 10; // multiplier * 10 == total number of key events let keysToSend = ''; - for (var i = 0; i <= keyMultiplier; i++) { + for (let i = 0; i <= keyMultiplier; i++) { keysToSend += chars; } @@ -47,23 +54,23 @@ xdescribe('Responsiveness of Editor', function () { textElement.sendkeys('{selectall}'); // select all textElement.sendkeys('{del}'); // clear the pad text - for (var i = 0; i <= amount; i++) { + for (let i = 0; i <= amount; i++) { text = `${text + chars} `; // add the chars and space to the text contents } inner$('div').first().text(text); // Put the text contents into the pad - helper.waitFor(() => // Wait for the new contents to be on the pad - inner$('div').text().length > length - ).done(() => { - expect(inner$('div').text().length).to.be.greaterThan(length); // has the text changed? + // Wait for the new contents to be on the pad + helper.waitFor(() => inner$('div').text().length > length).done(() => { + // has the text changed? + expect(inner$('div').text().length).to.be.greaterThan(length); const start = Date.now(); // get the start time // send some new text to the screen (ensure all 3 key events are sent) const el = inner$('div').first(); for (let i = 0; i < keysToSend.length; ++i) { - var x = keysToSend.charCodeAt(i); + const x = keysToSend.charCodeAt(i); ['keyup', 'keypress', 'keydown'].forEach((type) => { - const e = $.Event(type); + const e = new $.Event(type); e.keyCode = x; el.trigger(e); }); diff --git a/tests/frontend/specs/select_formatting_buttons.js b/tests/frontend/specs/select_formatting_buttons.js index 52595a044..358d9e5b7 100644 --- a/tests/frontend/specs/select_formatting_buttons.js +++ b/tests/frontend/specs/select_formatting_buttons.js @@ -1,3 +1,5 @@ +'use strict'; + describe('select formatting buttons when selection has style applied', function () { const STYLES = ['italic', 'bold', 'underline', 'strikethrough']; const SHORTCUT_KEYS = ['I', 'B', 'U', '5']; // italic, bold, underline, strikethrough @@ -21,7 +23,7 @@ describe('select formatting buttons when selection has style applied', function return $formattingButton.parent().hasClass('selected'); }; - var selectLine = function (lineNumber, offsetStart, offsetEnd) { + const selectLine = function (lineNumber, offsetStart, offsetEnd) { const inner$ = helper.padInner$; const $line = inner$('div').eq(lineNumber); helper.selectLines($line, $line, offsetStart, offsetEnd); @@ -58,7 +60,7 @@ describe('select formatting buttons when selection has style applied', function applyStyleOnLineOnFullLineAndRemoveSelection(line, style, placeCaretOnLine, cb); }; - var applyStyleOnLineOnFullLineAndRemoveSelection = function (line, style, selectTarget, cb) { + const applyStyleOnLineOnFullLineAndRemoveSelection = function (line, style, selectTarget, cb) { // see if line html has changed const inner$ = helper.padInner$; const oldLineHTML = inner$.find('div')[line]; @@ -80,7 +82,6 @@ describe('select formatting buttons when selection has style applied', function const pressFormattingShortcutOnSelection = function (key) { const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // get the first text element out of the inner iframe const $firstTextElement = inner$('div').first(); @@ -88,7 +89,7 @@ describe('select formatting buttons when selection has style applied', function // select this text element $firstTextElement.sendkeys('{selectall}'); - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = key.charCodeAt(0); // I, U, B, 5 inner$('#innerdocbody').trigger(e); diff --git a/tests/frontend/specs/strikethrough.js b/tests/frontend/specs/strikethrough.js index d8feae3be..9731ec75c 100644 --- a/tests/frontend/specs/strikethrough.js +++ b/tests/frontend/specs/strikethrough.js @@ -1,3 +1,5 @@ +'use strict'; + describe('strikethrough button', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -19,7 +21,7 @@ describe('strikethrough button', function () { const $strikethroughButton = chrome$('.buttonicon-strikethrough'); $strikethroughButton.click(); - // ace creates a new dom element when you press a button, so just get the first text element again + // ace creates a new dom element when you press a button, just get the first text element again const $newFirstTextElement = inner$('div').first(); // is there a element now? diff --git a/tests/frontend/specs/timeslider.js b/tests/frontend/specs/timeslider.js index bea7932df..10f94b3cb 100644 --- a/tests/frontend/specs/timeslider.js +++ b/tests/frontend/specs/timeslider.js @@ -1,3 +1,5 @@ +'use strict'; + // deactivated, we need a nice way to get the timeslider, this is ugly xdescribe('timeslider button takes you to the timeslider of a pad', function () { beforeEach(function (cb) { @@ -12,7 +14,6 @@ xdescribe('timeslider button takes you to the timeslider of a pad', function () // get the first text element inside the editable space const $firstTextElement = inner$('div span').first(); const originalValue = $firstTextElement.text(); // get the original value - const newValue = `Testing${originalValue}`; $firstTextElement.sendkeys('Testing'); // send line 1 to the pad const modifiedValue = $firstTextElement.text(); // get the modified value diff --git a/tests/frontend/specs/timeslider_labels.js b/tests/frontend/specs/timeslider_labels.js index c7a4aca5a..dd418d976 100644 --- a/tests/frontend/specs/timeslider_labels.js +++ b/tests/frontend/specs/timeslider_labels.js @@ -1,3 +1,5 @@ +'use strict'; + describe('timeslider', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -7,7 +9,7 @@ describe('timeslider', function () { /** * @todo test authorsList */ - it("Shows a date and time in the timeslider and make sure it doesn't include NaN", async function () { + it("Shows a date/time in the timeslider and make sure it doesn't include NaN", async function () { // make some changes to produce 3 revisions const revs = 3; diff --git a/tests/frontend/specs/timeslider_numeric_padID.js b/tests/frontend/specs/timeslider_numeric_padID.js index 53eb4a29c..4d05f95b3 100644 --- a/tests/frontend/specs/timeslider_numeric_padID.js +++ b/tests/frontend/specs/timeslider_numeric_padID.js @@ -1,3 +1,5 @@ +'use strict'; + describe('timeslider', function () { const padId = 735773577357 + (Math.round(Math.random() * 1000)); diff --git a/tests/frontend/specs/timeslider_revisions.js b/tests/frontend/specs/timeslider_revisions.js index fbfbb3615..2f5cc170a 100644 --- a/tests/frontend/specs/timeslider_revisions.js +++ b/tests/frontend/specs/timeslider_revisions.js @@ -1,3 +1,5 @@ +'use strict'; + describe('timeslider', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -23,7 +25,8 @@ describe('timeslider', function () { setTimeout(() => { // go to timeslider - $('#iframe-container iframe').attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider`); + $('#iframe-container iframe').attr('src', + `${$('#iframe-container iframe').attr('src')}/timeslider`); setTimeout(() => { const timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; @@ -66,7 +69,6 @@ describe('timeslider', function () { // Disabled as jquery trigger no longer works properly xit('changes the url when clicking on the timeslider', function (done) { const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // make some changes to produce 7 revisions const timePerRev = 1000; @@ -81,13 +83,13 @@ describe('timeslider', function () { setTimeout(() => { // go to timeslider - $('#iframe-container iframe').attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider`); + $('#iframe-container iframe').attr('src', + `${$('#iframe-container iframe').attr('src')}/timeslider`); setTimeout(() => { const timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; const $sliderBar = timeslider$('#ui-slider-bar'); - const latestContents = timeslider$('#innerdocbody').text(); const oldUrl = $('#iframe-container iframe')[0].contentWindow.location.hash; // Click somewhere on the timeslider @@ -96,20 +98,23 @@ describe('timeslider', function () { e.clientY = e.pageY = 60; $sliderBar.trigger(e); - helper.waitFor(() => $('#iframe-container iframe')[0].contentWindow.location.hash != oldUrl, 6000).always(() => { - expect($('#iframe-container iframe')[0].contentWindow.location.hash).not.to.eql(oldUrl); - done(); - }); + helper.waitFor( + () => $('#iframe-container iframe')[0].contentWindow.location.hash !== oldUrl, 6000) + .always(() => { + expect( + $('#iframe-container iframe')[0].contentWindow.location.hash + ).not.to.eql(oldUrl); + done(); + }); }, 6000); }, revs * timePerRev); }); it('jumps to a revision given in the url', function (done) { const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; this.timeout(40000); // wait for the text to be loaded - helper.waitFor(() => inner$('body').text().length != 0, 10000).always(() => { + helper.waitFor(() => inner$('body').text().length !== 0, 10000).always(() => { const newLines = inner$('body div').length; const oldLength = inner$('body').text().length + newLines / 2; expect(oldLength).to.not.eql(0); @@ -120,22 +125,25 @@ describe('timeslider', function () { helper.waitFor(() => { // newLines takes the new lines into account which are strippen when using // inner$('body').text(), one
        is used for one line in ACE. - const lenOkay = inner$('body').text().length + newLines / 2 != oldLength; + const lenOkay = inner$('body').text().length + newLines / 2 !== oldLength; // this waits for the color to be added to our , which means that the revision // was accepted by the server. - const colorOkay = inner$('span').first().attr('class').indexOf('author-') == 0; + const colorOkay = inner$('span').first().attr('class').indexOf('author-') === 0; return lenOkay && colorOkay; }, 10000).always(() => { // go to timeslider with a specific revision set - $('#iframe-container iframe').attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider#0`); + $('#iframe-container iframe').attr('src', + `${$('#iframe-container iframe').attr('src')}/timeslider#0`); // wait for the timeslider to be loaded helper.waitFor(() => { try { timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; - } catch (e) {} + } catch (e) { + // Empty catch block <3 + } if (timeslider$) { - return timeslider$('#innerdocbody').text().length == oldLength; + return timeslider$('#innerdocbody').text().length === oldLength; } }, 10000).always(() => { expect(timeslider$('#innerdocbody').text().length).to.eql(oldLength); @@ -147,24 +155,26 @@ describe('timeslider', function () { it('checks the export url', function (done) { const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; this.timeout(11000); inner$('div').first().sendkeys('a'); setTimeout(() => { // go to timeslider - $('#iframe-container iframe').attr('src', `${$('#iframe-container iframe').attr('src')}/timeslider#0`); + $('#iframe-container iframe').attr('src', + `${$('#iframe-container iframe').attr('src')}/timeslider#0`); let timeslider$; let exportLink; helper.waitFor(() => { try { timeslider$ = $('#iframe-container iframe')[0].contentWindow.$; - } catch (e) {} + } catch (e) { + // Empty catch block <3 + } if (!timeslider$) return false; exportLink = timeslider$('#exportplaina').attr('href'); if (!exportLink) return false; - return exportLink.substr(exportLink.length - 12) == '0/export/txt'; + return exportLink.substr(exportLink.length - 12) === '0/export/txt'; }, 6000).always(() => { expect(exportLink.substr(exportLink.length - 12)).to.eql('0/export/txt'); done(); diff --git a/tests/frontend/specs/undo.js b/tests/frontend/specs/undo.js index 0c94f2230..0daa282fc 100644 --- a/tests/frontend/specs/undo.js +++ b/tests/frontend/specs/undo.js @@ -1,3 +1,5 @@ +'use strict'; + describe('undo button', function () { beforeEach(function (cb) { helper.newPad(cb); // creates a new pad @@ -30,7 +32,6 @@ describe('undo button', function () { it('undo some typing using a keypress', function (done) { const inner$ = helper.padInner$; - const chrome$ = helper.padChrome$; // get the first text element inside the editable space const $firstTextElement = inner$('div span').first(); @@ -40,7 +41,7 @@ describe('undo button', function () { const modifiedValue = $firstTextElement.text(); // get the modified value expect(modifiedValue).not.to.be(originalValue); // expect the value to change - const e = inner$.Event(helper.evtType); + const e = new inner$.Event(helper.evtType); e.ctrlKey = true; // Control key e.which = 90; // z inner$('#innerdocbody').trigger(e); diff --git a/tests/frontend/specs/unordered_list.js b/tests/frontend/specs/unordered_list.js index 4cbdabfac..22d1a6fe2 100644 --- a/tests/frontend/specs/unordered_list.js +++ b/tests/frontend/specs/unordered_list.js @@ -1,3 +1,5 @@ +'use strict'; + describe('assign unordered list', function () { // create a new pad before each test run beforeEach(function (cb) { @@ -130,7 +132,8 @@ describe('Pressing Tab in an UL increases and decreases indentation', function ( }); }); -describe('Pressing indent/outdent button in an UL increases and decreases indentation and bullet / ol formatting', function () { +describe('Pressing indent/outdent button in an UL increases and decreases indentation ' + + 'and bullet / ol formatting', function () { // create a new pad before each test run beforeEach(function (cb) { helper.newPad(cb); diff --git a/tests/frontend/specs/xxauto_reconnect.js b/tests/frontend/specs/xxauto_reconnect.js index 574616ce5..d92936563 100644 --- a/tests/frontend/specs/xxauto_reconnect.js +++ b/tests/frontend/specs/xxauto_reconnect.js @@ -1,3 +1,5 @@ +'use strict'; + describe('Automatic pad reload on Force Reconnect message', function () { let padId, $originalPadFrame; diff --git a/tests/frontend/travis/remote_runner.js b/tests/frontend/travis/remote_runner.js index 70c850ca8..ba0e7be0c 100644 --- a/tests/frontend/travis/remote_runner.js +++ b/tests/frontend/travis/remote_runner.js @@ -1,17 +1,19 @@ -var srcFolder = '../../../src/node_modules/'; -var wd = require(`${srcFolder}wd`); -var async = require(`${srcFolder}async`); +'use strict'; -var config = { +const wd = require('ep_etherpad-lite/node_modules/wd'); +const async = require('ep_etherpad-lite/node_modules/async'); + +const config = { host: 'ondemand.saucelabs.com', port: 80, username: process.env.SAUCE_USER, accessKey: process.env.SAUCE_ACCESS_KEY, }; -var allTestsPassed = true; +let allTestsPassed = true; // overwrite the default exit code -// in case not all worker can be run (due to saucelabs limits), `queue.drain` below will not be called +// in case not all worker can be run (due to saucelabs limits), +// `queue.drain` below will not be called // and the script would silently exit with error code 0 process.exitCode = 2; process.on('exit', (code) => { @@ -20,13 +22,18 @@ process.on('exit', (code) => { } }); -var sauceTestWorker = async.queue((testSettings, callback) => { - const browser = wd.promiseChainRemote(config.host, config.port, config.username, config.accessKey); - const name = `${process.env.GIT_HASH} - ${testSettings.browserName} ${testSettings.version}, ${testSettings.platform}`; +const sauceTestWorker = async.queue((testSettings, callback) => { + const browser = wd.promiseChainRemote( + config.host, config.port, config.username, config.accessKey); + const name = + `${process.env.GIT_HASH} - ${testSettings.browserName} ` + + `${testSettings.version}, ${testSettings.platform}`; testSettings.name = name; testSettings.public = true; testSettings.build = process.env.GIT_HASH; - testSettings.extendedDebugging = true; // console.json can be downloaded via saucelabs, don't know how to print them into output of the tests + // console.json can be downloaded via saucelabs, + // don't know how to print them into output of the tests + testSettings.extendedDebugging = true; testSettings.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; browser.init(testSettings).get('http://localhost:9001/tests/frontend/', () => { @@ -34,7 +41,7 @@ var sauceTestWorker = async.queue((testSettings, callback) => { console.log(`Remote sauce test '${name}' started! ${url}`); // tear down the test excecution - const stopSauce = function (success, timesup) { + const stopSauce = (success, timesup) => { clearInterval(getStatusInterval); clearTimeout(timeout); @@ -43,12 +50,15 @@ var sauceTestWorker = async.queue((testSettings, callback) => { allTestsPassed = false; } - // if stopSauce is called via timeout (in contrast to via getStatusInterval) than the log of up to the last + // if stopSauce is called via timeout + // (in contrast to via getStatusInterval) than the log of up to the last // five seconds may not be available here. It's an error anyway, so don't care about it. printLog(logIndex); if (timesup) { - console.log(`[${testSettings.browserName} ${testSettings.platform}${testSettings.version === '' ? '' : (` ${testSettings.version}`)}] \x1B[31mFAILED\x1B[39m allowed test duration exceeded`); + console.log(`[${testSettings.browserName} ${testSettings.platform}` + + `${testSettings.version === '' ? '' : (` ${testSettings.version}`)}]` + + ' \x1B[31mFAILED\x1B[39m allowed test duration exceeded'); } console.log(`Remote sauce test '${name}' finished! ${url}`); @@ -58,17 +68,19 @@ var sauceTestWorker = async.queue((testSettings, callback) => { /** * timeout if a test hangs or the job exceeds 14.5 minutes - * It's necessary because if travis kills the saucelabs session due to inactivity, we don't get any output - * @todo this should be configured in testSettings, see https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-Timeouts + * It's necessary because if travis kills the saucelabs session due to inactivity, + * we don't get any output + * @todo this should be configured in testSettings, see + * https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-Timeouts */ - var timeout = setTimeout(() => { + const timeout = setTimeout(() => { stopSauce(false, true); }, 870000); // travis timeout is 15 minutes, set this to a slightly lower value let knownConsoleText = ''; // how many characters of the log have been sent to travis let logIndex = 0; - var getStatusInterval = setInterval(() => { + const getStatusInterval = setInterval(() => { browser.eval("$('#console').text()", (err, consoleText) => { if (!consoleText || err) { return; @@ -76,9 +88,10 @@ var sauceTestWorker = async.queue((testSettings, callback) => { knownConsoleText = consoleText; if (knownConsoleText.indexOf('FINISHED') > 0) { - const match = knownConsoleText.match(/FINISHED.*([0-9]+) tests passed, ([0-9]+) tests failed/); + const match = knownConsoleText.match( + /FINISHED.*([0-9]+) tests passed, ([0-9]+) tests failed/); // finished without failures - if (match[2] && match[2] == '0') { + if (match[2] && match[2] === '0') { stopSauce(true); // finished but some tests did not return or some tests failed @@ -99,13 +112,17 @@ var sauceTestWorker = async.queue((testSettings, callback) => { * * @param {number} index offset from where to start */ - function printLog(index) { - let testResult = knownConsoleText.substring(index).replace(/\[red\]/g, '\x1B[31m').replace(/\[yellow\]/g, '\x1B[33m') + const printLog = (index) => { + let testResult = knownConsoleText.substring(index) + .replace(/\[red\]/g, '\x1B[31m').replace(/\[yellow\]/g, '\x1B[33m') .replace(/\[green\]/g, '\x1B[32m').replace(/\[clear\]/g, '\x1B[39m'); - testResult = testResult.split('\\n').map((line) => `[${testSettings.browserName} ${testSettings.platform}${testSettings.version === '' ? '' : (` ${testSettings.version}`)}] ${line}`).join('\n'); + testResult = testResult.split('\\n').map((line) => `[${testSettings.browserName} ` + + `${testSettings.platform}` + + `${testSettings.version === '' ? '' : (` ${testSettings.version}`)}]` + + `${line}`).join('\n'); console.log(testResult); - } + }; }); }, 6); // run 6 tests in parrallel diff --git a/tests/ratelimit/send_changesets.js b/tests/ratelimit/send_changesets.js index b0d994c8c..92af23e18 100644 --- a/tests/ratelimit/send_changesets.js +++ b/tests/ratelimit/send_changesets.js @@ -1,8 +1,12 @@ +'use strict'; + +let etherpad; try { - var etherpad = require('../../src/node_modules/etherpad-cli-client'); + etherpad = require('ep_etherpad-lite/node_modules/etherpad-cli-client'); // ugly } catch { - var etherpad = require('etherpad-cli-client'); + /* eslint-disable-next-line node/no-missing-require */ + etherpad = require('etherpad-cli-client'); // uses global } const pad = etherpad.connect(process.argv[2]); pad.on('connected', () => { @@ -18,7 +22,7 @@ pad.on('connected', () => { }); // in case of disconnect exit code 1 pad.on('message', (message) => { - if (message.disconnect == 'rateLimited') { + if (message.disconnect === 'rateLimited') { process.exit(1); } }); From 3e910b990568eb7afe33e75511b0771c5a161340 Mon Sep 17 00:00:00 2001 From: John McLear Date: Mon, 1 Feb 2021 22:45:51 +0000 Subject: [PATCH 07/31] stale: remove convert.js as no one runs old Etherpad --- bin/convert.js | 391 ------------------------------------------------- 1 file changed, 391 deletions(-) delete mode 100644 bin/convert.js diff --git a/bin/convert.js b/bin/convert.js deleted file mode 100644 index 47f8b2d27..000000000 --- a/bin/convert.js +++ /dev/null @@ -1,391 +0,0 @@ -const startTime = Date.now(); -const fs = require('fs'); -const ueberDB = require('../src/node_modules/ueberdb2'); -const mysql = require('../src/node_modules/ueberdb2/node_modules/mysql'); -const async = require('../src/node_modules/async'); -const Changeset = require('ep_etherpad-lite/static/js/Changeset'); -const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; -const AttributePool = require('ep_etherpad-lite/static/js/AttributePool'); - -const settingsFile = process.argv[2]; -const sqlOutputFile = process.argv[3]; - -// stop if the settings file is not set -if (!settingsFile || !sqlOutputFile) { - console.error('Use: node convert.js $SETTINGSFILE $SQLOUTPUT'); - process.exit(1); -} - -log('read settings file...'); -// read the settings file and parse the json -const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8')); -log('done'); - -log('open output file...'); -const sqlOutput = fs.openSync(sqlOutputFile, 'w'); -const sql = 'SET CHARACTER SET UTF8;\n' + - 'CREATE TABLE IF NOT EXISTS `store` ( \n' + - '`key` VARCHAR( 100 ) NOT NULL , \n' + - '`value` LONGTEXT NOT NULL , \n' + - 'PRIMARY KEY ( `key` ) \n' + - ') ENGINE = INNODB;\n' + - 'START TRANSACTION;\n\n'; -fs.writeSync(sqlOutput, sql); -log('done'); - -const etherpadDB = mysql.createConnection({ - host: settings.etherpadDB.host, - user: settings.etherpadDB.user, - password: settings.etherpadDB.password, - database: settings.etherpadDB.database, - port: settings.etherpadDB.port, -}); - -// get the timestamp once -const timestamp = Date.now(); - -let padIDs; - -async.series([ - // get all padids out of the database... - function (callback) { - log('get all padIds out of the database...'); - - etherpadDB.query('SELECT ID FROM PAD_META', [], (err, _padIDs) => { - padIDs = _padIDs; - callback(err); - }); - }, - function (callback) { - log('done'); - - // create a queue with a concurrency 100 - const queue = async.queue((padId, callback) => { - convertPad(padId, (err) => { - incrementPadStats(); - callback(err); - }); - }, 100); - - // set the step callback as the queue callback - queue.drain = callback; - - // add the padids to the worker queue - for (let i = 0, length = padIDs.length; i < length; i++) { - queue.push(padIDs[i].ID); - } - }, -], (err) => { - if (err) throw err; - - // write the groups - let sql = ''; - for (const proID in proID2groupID) { - const groupID = proID2groupID[proID]; - const subdomain = proID2subdomain[proID]; - - sql += `REPLACE INTO store VALUES (${etherpadDB.escape(`group:${groupID}`)}, ${etherpadDB.escape(JSON.stringify(groups[groupID]))});\n`; - sql += `REPLACE INTO store VALUES (${etherpadDB.escape(`mapper2group:subdomain:${subdomain}`)}, ${etherpadDB.escape(groupID)});\n`; - } - - // close transaction - sql += 'COMMIT;'; - - // end the sql file - fs.writeSync(sqlOutput, sql, undefined, 'utf-8'); - fs.closeSync(sqlOutput); - - log('finished.'); - process.exit(0); -}); - -function log(str) { - console.log(`${(Date.now() - startTime) / 1000}\t${str}`); -} - -let padsDone = 0; - -function incrementPadStats() { - padsDone++; - - if (padsDone % 100 == 0) { - const averageTime = Math.round(padsDone / ((Date.now() - startTime) / 1000)); - log(`${padsDone}/${padIDs.length}\t${averageTime} pad/s`); - } -} - -var proID2groupID = {}; -var proID2subdomain = {}; -var groups = {}; - -function convertPad(padId, callback) { - const changesets = []; - const changesetsMeta = []; - const chatMessages = []; - const authors = []; - let apool; - let subdomain; - let padmeta; - - async.series([ - // get all needed db values - function (callback) { - async.parallel([ - // get the pad revisions - function (callback) { - const sql = 'SELECT * FROM `PAD_CHAT_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_CHAT_META` WHERE ID=?)'; - - etherpadDB.query(sql, [padId], (err, results) => { - if (!err) { - try { - // parse the pages - for (let i = 0, length = results.length; i < length; i++) { - parsePage(chatMessages, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true); - } - } catch (e) { err = e; } - } - - callback(err); - }); - }, - // get the chat entries - function (callback) { - const sql = 'SELECT * FROM `PAD_REVS_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_REVS_META` WHERE ID=?)'; - - etherpadDB.query(sql, [padId], (err, results) => { - if (!err) { - try { - // parse the pages - for (let i = 0, length = results.length; i < length; i++) { - parsePage(changesets, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, false); - } - } catch (e) { err = e; } - } - - callback(err); - }); - }, - // get the pad revisions meta data - function (callback) { - const sql = 'SELECT * FROM `PAD_REVMETA_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_REVMETA_META` WHERE ID=?)'; - - etherpadDB.query(sql, [padId], (err, results) => { - if (!err) { - try { - // parse the pages - for (let i = 0, length = results.length; i < length; i++) { - parsePage(changesetsMeta, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true); - } - } catch (e) { err = e; } - } - - callback(err); - }); - }, - // get the attribute pool of this pad - function (callback) { - const sql = 'SELECT `JSON` FROM `PAD_APOOL` WHERE `ID` = ?'; - - etherpadDB.query(sql, [padId], (err, results) => { - if (!err) { - try { - apool = JSON.parse(results[0].JSON).x; - } catch (e) { err = e; } - } - - callback(err); - }); - }, - // get the authors informations - function (callback) { - const sql = 'SELECT * FROM `PAD_AUTHORS_TEXT` WHERE NUMID = (SELECT `NUMID` FROM `PAD_AUTHORS_META` WHERE ID=?)'; - - etherpadDB.query(sql, [padId], (err, results) => { - if (!err) { - try { - // parse the pages - for (let i = 0, length = results.length; i < length; i++) { - parsePage(authors, results[i].PAGESTART, results[i].OFFSETS, results[i].DATA, true); - } - } catch (e) { err = e; } - } - - callback(err); - }); - }, - // get the pad information - function (callback) { - const sql = 'SELECT JSON FROM `PAD_META` WHERE ID=?'; - - etherpadDB.query(sql, [padId], (err, results) => { - if (!err) { - try { - padmeta = JSON.parse(results[0].JSON).x; - } catch (e) { err = e; } - } - - callback(err); - }); - }, - // get the subdomain - function (callback) { - // skip if this is no proPad - if (padId.indexOf('$') == -1) { - callback(); - return; - } - - // get the proID out of this padID - const proID = padId.split('$')[0]; - - const sql = 'SELECT subDomain FROM pro_domains WHERE ID = ?'; - - etherpadDB.query(sql, [proID], (err, results) => { - if (!err) { - subdomain = results[0].subDomain; - } - - callback(err); - }); - }, - ], callback); - }, - function (callback) { - // saves all values that should be written to the database - const values = {}; - - // this is a pro pad, let's convert it to a group pad - if (padId.indexOf('$') != -1) { - const padIdParts = padId.split('$'); - const proID = padIdParts[0]; - const padName = padIdParts[1]; - - let groupID; - - // this proID is not converted so far, do it - if (proID2groupID[proID] == null) { - groupID = `g.${randomString(16)}`; - - // create the mappers for this new group - proID2groupID[proID] = groupID; - proID2subdomain[proID] = subdomain; - groups[groupID] = {pads: {}}; - } - - // use the generated groupID; - groupID = proID2groupID[proID]; - - // rename the pad - padId = `${groupID}$${padName}`; - - // set the value for this pad in the group - groups[groupID].pads[padId] = 1; - } - - try { - const newAuthorIDs = {}; - const oldName2newName = {}; - - // replace the authors with generated authors - // we need to do that cause where the original etherpad saves pad local authors, the new (lite) etherpad uses them global - for (var i in apool.numToAttrib) { - var key = apool.numToAttrib[i][0]; - const value = apool.numToAttrib[i][1]; - - // skip non authors and anonymous authors - if (key != 'author' || value == '') continue; - - // generate new author values - const authorID = `a.${randomString(16)}`; - const authorColorID = authors[i].colorId || Math.floor(Math.random() * (exports.getColorPalette().length)); - const authorName = authors[i].name || null; - - // overwrite the authorID of the attribute pool - apool.numToAttrib[i][1] = authorID; - - // write the author to the database - values[`globalAuthor:${authorID}`] = {colorId: authorColorID, name: authorName, timestamp}; - - // save in mappers - newAuthorIDs[i] = authorID; - oldName2newName[value] = authorID; - } - - // save all revisions - for (var i = 0; i < changesets.length; i++) { - values[`pad:${padId}:revs:${i}`] = {changeset: changesets[i], - meta: { - author: newAuthorIDs[changesetsMeta[i].a], - timestamp: changesetsMeta[i].t, - atext: changesetsMeta[i].atext || undefined, - }}; - } - - // save all chat messages - for (var i = 0; i < chatMessages.length; i++) { - values[`pad:${padId}:chat:${i}`] = {text: chatMessages[i].lineText, - userId: oldName2newName[chatMessages[i].userId], - time: chatMessages[i].time}; - } - - // generate the latest atext - const fullAPool = (new AttributePool()).fromJsonable(apool); - const keyRev = Math.floor(padmeta.head / padmeta.keyRevInterval) * padmeta.keyRevInterval; - let atext = changesetsMeta[keyRev].atext; - let curRev = keyRev; - while (curRev < padmeta.head) { - curRev++; - const changeset = changesets[curRev]; - atext = Changeset.applyToAText(changeset, atext, fullAPool); - } - - values[`pad:${padId}`] = {atext, - pool: apool, - head: padmeta.head, - chatHead: padmeta.numChatMessages}; - } catch (e) { - console.error(`Error while converting pad ${padId}, pad skipped`); - console.error(e.stack ? e.stack : JSON.stringify(e)); - callback(); - return; - } - - let sql = ''; - for (var key in values) { - sql += `REPLACE INTO store VALUES (${etherpadDB.escape(key)}, ${etherpadDB.escape(JSON.stringify(values[key]))});\n`; - } - - fs.writeSync(sqlOutput, sql, undefined, 'utf-8'); - callback(); - }, - ], callback); -} - -/** - * This parses a Page like Etherpad uses them in the databases - * The offsets describes the length of a unit in the page, the data are - * all values behind each other - */ -function parsePage(array, pageStart, offsets, data, json) { - let start = 0; - const lengths = offsets.split(','); - - for (let i = 0; i < lengths.length; i++) { - let unitLength = lengths[i]; - - // skip empty units - if (unitLength == '') continue; - - // parse the number - unitLength = Number(unitLength); - - // cut the unit out of data - const unit = data.substr(start, unitLength); - - // put it into the array - array[pageStart + i] = json ? JSON.parse(unit) : unit; - - // update start - start += unitLength; - } -} From 1bc52f49134ffffadde61264a6f2d3cbb001af37 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sat, 30 Jan 2021 20:29:49 -0500 Subject: [PATCH 08/31] hooks: Remove unnecessary `callAllStr()` function --- src/static/js/linestylefilter.js | 7 ++----- src/static/js/pluginfw/hooks.js | 12 ------------ 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js index f70eefc23..254168990 100644 --- a/src/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.js @@ -93,11 +93,8 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool } else if (linestylefilter.ATTRIB_CLASSES[key]) { classes += ` ${linestylefilter.ATTRIB_CLASSES[key]}`; } else { - classes += hooks.callAllStr('aceAttribsToClasses', { - linestylefilter, - key, - value, - }, ' ', ' ', ''); + const results = hooks.callAll('aceAttribsToClasses', {linestylefilter, key, value}); + classes += ` ${results.join(' ')}`; } } } diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index e61079eb6..e0b733e5d 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -386,18 +386,6 @@ exports.aCallFirst = (hook_name, args, cb, predicate) => { } }; -exports.callAllStr = (hook_name, args, sep, pre, post) => { - if (sep === undefined) sep = ''; - if (pre === undefined) pre = ''; - if (post === undefined) post = ''; - const newCallhooks = []; - const callhooks = exports.callAll(hook_name, args); - for (let i = 0, ii = callhooks.length; i < ii; i++) { - newCallhooks[i] = pre + callhooks[i] + post; - } - return newCallhooks.join(sep || ''); -}; - exports.exportedForTestingOnly = { callHookFnAsync, callHookFnSync, From 47f0a7dacfa811c4c69605c207f4e353c96b1a0a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 31 Jan 2021 15:23:44 -0500 Subject: [PATCH 09/31] lint: Fix more ESLint errors --- src/static/js/pluginfw/hooks.js | 38 +++++++++++++++++--------------- tests/backend/specs/hooks.js | 17 +++++--------- tests/backend/specs/webaccess.js | 10 ++++----- 3 files changed, 30 insertions(+), 35 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index e0b733e5d..7ce71ae7b 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -1,6 +1,5 @@ 'use strict'; -const _ = require('underscore'); const pluginDefs = require('./plugin_defs'); // Maps the name of a server-side hook to a string explaining the deprecation @@ -24,9 +23,12 @@ const checkDeprecation = (hook) => { deprecationWarned[hook.hook_fn_name] = true; }; +// Flattens the array one level. +const flatten1 = (array) => array.reduce((a, b) => a.concat(b), []); + exports.bubbleExceptions = true; -const hookCallWrapper = (hook, hook_name, args, cb) => { +const hookCallWrapper = (hook, hookName, args, cb) => { if (cb === undefined) cb = (x) => x; checkDeprecation(hook); @@ -36,7 +38,7 @@ const hookCallWrapper = (hook, hook_name, args, cb) => { if (x === undefined) return []; return x; }; - const normalizedhook = () => normalize(hook.hook_fn(hook_name, args, (x) => cb(normalize(x)))); + const normalizedhook = () => normalize(hook.hook_fn(hookName, args, (x) => cb(normalize(x)))); if (exports.bubbleExceptions) { return normalizedhook(); @@ -44,7 +46,7 @@ const hookCallWrapper = (hook, hook_name, args, cb) => { try { return normalizedhook(); } catch (ex) { - console.error([hook_name, hook.part.full_name, ex.stack || ex]); + console.error([hookName, hook.part.full_name, ex.stack || ex]); } } }; @@ -204,12 +206,12 @@ const callHookFnSync = (hook, context) => { exports.callAll = (hookName, context) => { if (context == null) context = {}; const hooks = pluginDefs.hooks[hookName] || []; - return _.flatten(hooks.map((hook) => { + return flatten1(hooks.map((hook) => { 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 @@ -349,26 +351,26 @@ exports.aCallAll = async (hookName, context, cb) => { let resultsPromise = Promise.all(hooks.map((hook) => callHookFnAsync(hook, context) // `undefined` (but not `null`!) is treated the same as []. .then((result) => (result === undefined) ? [] : result))) - .then((results) => _.flatten(results, 1)); + .then(flatten1); if (cb != null) resultsPromise = resultsPromise.then((val) => cb(null, val), cb); return await resultsPromise; }; -exports.callFirst = (hook_name, args) => { +exports.callFirst = (hookName, args) => { if (!args) args = {}; - if (pluginDefs.hooks[hook_name] === undefined) return []; - return exports.syncMapFirst(pluginDefs.hooks[hook_name], - (hook) => hookCallWrapper(hook, hook_name, args)); + if (pluginDefs.hooks[hookName] === undefined) return []; + return exports.syncMapFirst(pluginDefs.hooks[hookName], + (hook) => hookCallWrapper(hook, hookName, args)); }; -const aCallFirst = (hook_name, args, cb, predicate) => { +const aCallFirst = (hookName, args, cb, predicate) => { if (!args) args = {}; if (!cb) cb = () => {}; - if (pluginDefs.hooks[hook_name] === undefined) return cb(null, []); + if (pluginDefs.hooks[hookName] === undefined) return cb(null, []); exports.mapFirst( - pluginDefs.hooks[hook_name], + pluginDefs.hooks[hookName], (hook, cb) => { - hookCallWrapper(hook, hook_name, args, (res) => { cb(null, res); }); + hookCallWrapper(hook, hookName, args, (res) => { cb(null, res); }); }, cb, predicate @@ -376,13 +378,13 @@ const aCallFirst = (hook_name, args, cb, predicate) => { }; /* return a Promise if cb is not supplied */ -exports.aCallFirst = (hook_name, args, cb, predicate) => { +exports.aCallFirst = (hookName, args, cb, predicate) => { if (cb === undefined) { return new Promise((resolve, reject) => { - aCallFirst(hook_name, args, (err, res) => err ? reject(err) : resolve(res), predicate); + aCallFirst(hookName, args, (err, res) => err ? reject(err) : resolve(res), predicate); }); } else { - return aCallFirst(hook_name, args, cb, predicate); + return aCallFirst(hookName, args, cb, predicate); } }; diff --git a/tests/backend/specs/hooks.js b/tests/backend/specs/hooks.js index 0e0f8075f..327b9bd1c 100644 --- a/tests/backend/specs/hooks.js +++ b/tests/backend/specs/hooks.js @@ -1,14 +1,9 @@ -/* global __dirname, __filename, afterEach, beforeEach, describe, it, process, require */ - -function m(mod) { return `${__dirname}/../../../src/${mod}`; } +'use strict'; const assert = require('assert').strict; -const common = require('../common'); -const hooks = require(m('static/js/pluginfw/hooks')); -const plugins = require(m('static/js/pluginfw/plugin_defs')); -const sinon = require(m('node_modules/sinon')); - -const logger = common.logger; +const hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); +const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs'); +const sinon = require('ep_etherpad-lite/node_modules/sinon'); describe(__filename, function () { const hookName = 'testHook'; @@ -203,7 +198,7 @@ describe(__filename, function () { // Test various ways a hook might attempt to settle twice. (Examples: call the callback a second // time, or call the callback and then return a value.) describe('bad hook function behavior (double settle)', function () { - beforeEach(function () { + beforeEach(async function () { sinon.stub(console, 'error'); }); @@ -558,7 +553,7 @@ describe(__filename, function () { // Test various ways a hook might attempt to settle twice. (Examples: call the callback a second // time, or call the callback and then return a value.) describe('bad hook function behavior (double settle)', function () { - beforeEach(function () { + beforeEach(async function () { sinon.stub(console, 'error'); }); diff --git a/tests/backend/specs/webaccess.js b/tests/backend/specs/webaccess.js index a21cc73a8..3b140c46c 100644 --- a/tests/backend/specs/webaccess.js +++ b/tests/backend/specs/webaccess.js @@ -1,11 +1,9 @@ -/* global __dirname, __filename, Buffer, afterEach, before, beforeEach, describe, it, require */ - -function m(mod) { return `${__dirname}/../../../src/${mod}`; } +'use strict'; const assert = require('assert').strict; const common = require('../common'); -const plugins = require(m('static/js/pluginfw/plugin_defs')); -const settings = require(m('node/utils/Settings')); +const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs'); +const settings = require('ep_etherpad-lite/node/utils/Settings'); describe(__filename, function () { let agent; @@ -402,7 +400,7 @@ describe(__filename, function () { }; const handlers = {}; - beforeEach(function () { + beforeEach(async function () { failHookNames.forEach((hookName) => { const handler = new Handler(hookName); handlers[hookName] = handler; From ba02e700201f4d94c233c1b2211414ee978cdfa5 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 1 Feb 2021 23:55:16 -0500 Subject: [PATCH 10/31] tests: Make the fake webaccess hook registrations look more real The additional properties will be needed once `aCallAll()` is upgraded to use `callHookFnAsync()`. --- tests/backend/specs/webaccess.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/backend/specs/webaccess.js b/tests/backend/specs/webaccess.js index 3b140c46c..b82a5f017 100644 --- a/tests/backend/specs/webaccess.js +++ b/tests/backend/specs/webaccess.js @@ -10,6 +10,13 @@ describe(__filename, function () { const backups = {}; const authHookNames = ['preAuthorize', 'authenticate', 'authorize']; const failHookNames = ['preAuthzFailure', 'authnFailure', 'authzFailure', 'authFailure']; + const makeHook = (hookName, hookFn) => ({ + hook_fn: hookFn, + hook_fn_name: `fake_plugin/${hookName}`, + hook_name: hookName, + part: {plugin: 'fake_plugin'}, + }); + before(async function () { agent = await common.init(); }); beforeEach(async function () { backups.hooks = {}; @@ -139,7 +146,10 @@ describe(__filename, function () { const h0 = new Handler(hookName, '_0'); const h1 = new Handler(hookName, '_1'); handlers[hookName] = [h0, h1]; - plugins.hooks[hookName] = [{hook_fn: h0.handle.bind(h0)}, {hook_fn: h1.handle.bind(h1)}]; + plugins.hooks[hookName] = [ + makeHook(hookName, h0.handle.bind(h0)), + makeHook(hookName, h1.handle.bind(h1)), + ]; } }); @@ -195,7 +205,7 @@ describe(__filename, function () { it('runs preAuthzFailure hook when access is denied', async function () { handlers.preAuthorize[0].innerHandle = () => [false]; let called = false; - plugins.hooks.preAuthzFailure = [{hook_fn: (hookName, {req, res}, cb) => { + plugins.hooks.preAuthzFailure = [makeHook('preAuthzFailure', (hookName, {req, res}, cb) => { assert.equal(hookName, 'preAuthzFailure'); assert(req != null); assert(res != null); @@ -203,7 +213,7 @@ describe(__filename, function () { called = true; res.status(200).send('injected'); return cb([true]); - }}]; + })]; await agent.get('/admin/').auth('admin', 'admin-password').expect(200, 'injected'); assert(called); }); @@ -404,7 +414,7 @@ describe(__filename, function () { failHookNames.forEach((hookName) => { const handler = new Handler(hookName); handlers[hookName] = handler; - plugins.hooks[hookName] = [{hook_fn: handler.handle.bind(handler)}]; + plugins.hooks[hookName] = [makeHook(hookName, handler.handle.bind(handler))]; }); settings.requireAuthentication = true; settings.requireAuthorization = true; From 6b42dabf6c3e3e72dd4c5571c20cd2b418fba2c8 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 31 Jan 2021 15:28:06 -0500 Subject: [PATCH 11/31] hooks: Delete unused `bubbleExceptions` setting --- src/static/js/pluginfw/hooks.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 7ce71ae7b..489e6d6dc 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -26,8 +26,6 @@ const checkDeprecation = (hook) => { // Flattens the array one level. const flatten1 = (array) => array.reduce((a, b) => a.concat(b), []); -exports.bubbleExceptions = true; - const hookCallWrapper = (hook, hookName, args, cb) => { if (cb === undefined) cb = (x) => x; @@ -38,17 +36,7 @@ const hookCallWrapper = (hook, hookName, args, cb) => { if (x === undefined) return []; return x; }; - const normalizedhook = () => normalize(hook.hook_fn(hookName, args, (x) => cb(normalize(x)))); - - if (exports.bubbleExceptions) { - return normalizedhook(); - } else { - try { - return normalizedhook(); - } catch (ex) { - console.error([hookName, hook.part.full_name, ex.stack || ex]); - } - } + return () => normalize(hook.hook_fn(hookName, args, (x) => cb(normalize(x)))); }; exports.syncMapFirst = (lst, fn) => { From 7dba847f21f8969e52f39d336cba6254426054f5 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 31 Jan 2021 16:49:17 -0500 Subject: [PATCH 12/31] hooks: Don't export `syncMapFirst` or `mapFirst` Nobody uses these functions outside of this file. --- src/static/js/pluginfw/hooks.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 489e6d6dc..7d7e1dd43 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -39,7 +39,7 @@ const hookCallWrapper = (hook, hookName, args, cb) => { return () => normalize(hook.hook_fn(hookName, args, (x) => cb(normalize(x)))); }; -exports.syncMapFirst = (lst, fn) => { +const syncMapFirst = (lst, fn) => { let i; let result; for (i = 0; i < lst.length; i++) { @@ -49,7 +49,7 @@ exports.syncMapFirst = (lst, fn) => { return []; }; -exports.mapFirst = (lst, fn, cb, predicate) => { +const mapFirst = (lst, fn, cb, predicate) => { if (predicate == null) predicate = (x) => (x != null && x.length > 0); let i = 0; @@ -347,7 +347,7 @@ exports.aCallAll = async (hookName, context, cb) => { exports.callFirst = (hookName, args) => { if (!args) args = {}; if (pluginDefs.hooks[hookName] === undefined) return []; - return exports.syncMapFirst(pluginDefs.hooks[hookName], + return syncMapFirst(pluginDefs.hooks[hookName], (hook) => hookCallWrapper(hook, hookName, args)); }; @@ -355,7 +355,7 @@ const aCallFirst = (hookName, args, cb, predicate) => { if (!args) args = {}; if (!cb) cb = () => {}; if (pluginDefs.hooks[hookName] === undefined) return cb(null, []); - exports.mapFirst( + mapFirst( pluginDefs.hooks[hookName], (hook, cb) => { hookCallWrapper(hook, hookName, args, (res) => { cb(null, res); }); From f02f288e8012538061a3973b42f2d4b8d1113c32 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 31 Jan 2021 18:07:36 -0500 Subject: [PATCH 13/31] hooks: Rename `args` to `context` for consistency --- src/static/js/pluginfw/hooks.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 7d7e1dd43..8434906b5 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -26,7 +26,7 @@ const checkDeprecation = (hook) => { // Flattens the array one level. const flatten1 = (array) => array.reduce((a, b) => a.concat(b), []); -const hookCallWrapper = (hook, hookName, args, cb) => { +const hookCallWrapper = (hook, hookName, context, cb) => { if (cb === undefined) cb = (x) => x; checkDeprecation(hook); @@ -36,7 +36,7 @@ const hookCallWrapper = (hook, hookName, args, cb) => { if (x === undefined) return []; return x; }; - return () => normalize(hook.hook_fn(hookName, args, (x) => cb(normalize(x)))); + return () => normalize(hook.hook_fn(hookName, context, (x) => cb(normalize(x)))); }; const syncMapFirst = (lst, fn) => { @@ -344,21 +344,21 @@ exports.aCallAll = async (hookName, context, cb) => { return await resultsPromise; }; -exports.callFirst = (hookName, args) => { - if (!args) args = {}; +exports.callFirst = (hookName, context) => { + if (!context) context = {}; if (pluginDefs.hooks[hookName] === undefined) return []; return syncMapFirst(pluginDefs.hooks[hookName], - (hook) => hookCallWrapper(hook, hookName, args)); + (hook) => hookCallWrapper(hook, hookName, context)); }; -const aCallFirst = (hookName, args, cb, predicate) => { - if (!args) args = {}; +const aCallFirst = (hookName, context, cb, predicate) => { + if (!context) context = {}; if (!cb) cb = () => {}; if (pluginDefs.hooks[hookName] === undefined) return cb(null, []); mapFirst( pluginDefs.hooks[hookName], (hook, cb) => { - hookCallWrapper(hook, hookName, args, (res) => { cb(null, res); }); + hookCallWrapper(hook, hookName, context, (res) => { cb(null, res); }); }, cb, predicate @@ -366,13 +366,13 @@ const aCallFirst = (hookName, args, cb, predicate) => { }; /* return a Promise if cb is not supplied */ -exports.aCallFirst = (hookName, args, cb, predicate) => { +exports.aCallFirst = (hookName, context, cb, predicate) => { if (cb === undefined) { return new Promise((resolve, reject) => { - aCallFirst(hookName, args, (err, res) => err ? reject(err) : resolve(res), predicate); + aCallFirst(hookName, context, (err, res) => err ? reject(err) : resolve(res), predicate); }); } else { - return aCallFirst(hookName, args, cb, predicate); + return aCallFirst(hookName, context, cb, predicate); } }; From c89db33ff038d3744bc907b8ce4e944b1556be59 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 31 Jan 2021 16:15:52 -0500 Subject: [PATCH 14/31] hooks: Refine caveat comments about function parameter count --- src/static/js/pluginfw/hooks.js | 40 ++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 8434906b5..5e8d9e73b 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -165,14 +165,27 @@ const callHookFnSync = (hook, context) => { // The hook function is assumed to not have a callback parameter, so fall through and accept // `undefined` as the resolved value. // - // IMPORTANT: "Rest" parameters and default parameters are not counted in`Function.length`, so - // the assumption does not hold for wrappers like `(...args) => { real(...args); }`. Such - // functions will still work properly without any logged warnings or errors for now, but: + // IMPORTANT: "Rest" parameters and default parameters are not included in `Function.length`, + // so the assumption does not hold for wrappers such as: + // + // const wrapper = (...args) => real(...args); + // + // ECMAScript does not provide a way to determine whether a function has default or rest + // parameters, so there is no way to be certain that a hook function with `length` < 3 will + // not call the callback. Synchronous hook functions that call the callback even though + // `length` < 3 will still work properly without any logged warnings or errors, but: + // // * Once the hook is upgraded to support asynchronous hook functions, calling the callback - // will (eventually) cause a double settle error, and the function might prematurely + // asynchronously will cause a double settle error, and the hook function will prematurely // resolve to `undefined` instead of the desired value. + // // * The above "unsettled function" warning is not logged if the function fails to call the // callback like it is supposed to. + // + // Wrapper functions can avoid problems by setting the wrapper's `length` property to match + // the real function's `length` property: + // + // Object.defineProperty(wrapper, 'length', {value: real.length}); } } @@ -300,10 +313,21 @@ const callHookFnAsync = async (hook, context) => { // The hook function is assumed to not have a callback parameter, so fall through and accept // `undefined` as the resolved value. // - // IMPORTANT: "Rest" parameters and default parameters are not counted in `Function.length`, - // so the assumption does not hold for wrappers like `(...args) => { real(...args); }`. For - // such functions, calling the callback will (eventually) cause a double settle error, and - // the function might prematurely resolve to `undefined` instead of the desired value. + // IMPORTANT: "Rest" parameters and default parameters are not included in + // `Function.length`, so the assumption does not hold for wrappers such as: + // + // const wrapper = (...args) => real(...args); + // + // ECMAScript does not provide a way to determine whether a function has default or rest + // parameters, so there is no way to be certain that a hook function with `length` < 3 will + // not call the callback. Hook functions with `length` < 3 that call the callback + // asynchronously will cause a double settle error, and the hook function will prematurely + // resolve to `undefined` instead of the desired value. + // + // Wrapper functions can avoid problems by setting the wrapper's `length` property to match + // the real function's `length` property: + // + // Object.defineProperty(wrapper, 'length', {value: real.length}); } } From 0b83ff8ec286087aa9ee9f826d31d289be89e03a Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 31 Jan 2021 16:30:55 -0500 Subject: [PATCH 15/31] hooks: Simplify `syncMapFirst` iteration --- src/static/js/pluginfw/hooks.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 5e8d9e73b..98b2af4c8 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -39,12 +39,11 @@ const hookCallWrapper = (hook, hookName, context, cb) => { return () => normalize(hook.hook_fn(hookName, context, (x) => cb(normalize(x)))); }; -const syncMapFirst = (lst, fn) => { - let i; - let result; - for (i = 0; i < lst.length; i++) { - result = fn(lst[i]); - if (result.length) return result; +const syncMapFirst = (hooks, fn) => { + const predicate = (val) => val.length; + for (const hook of hooks) { + const val = fn(hook); + if (predicate(val)) return val; } return []; }; From 53ccfa87030f795366df53050a483921900ffe5c Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 31 Jan 2021 16:51:37 -0500 Subject: [PATCH 16/31] hooks: Asyncify `mapFirst` --- src/static/js/pluginfw/hooks.js | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 98b2af4c8..de7ddc32c 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -1,6 +1,7 @@ 'use strict'; const pluginDefs = require('./plugin_defs'); +const util = require('util'); // Maps the name of a server-side hook to a string explaining the deprecation // (e.g., 'use the foo hook instead'). @@ -48,19 +49,13 @@ const syncMapFirst = (hooks, fn) => { return []; }; -const mapFirst = (lst, fn, cb, predicate) => { - if (predicate == null) predicate = (x) => (x != null && x.length > 0); - let i = 0; - - const next = () => { - 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(); +const mapFirst = async (hooks, fn, predicate = null) => { + if (predicate == null) predicate = (val) => val.length; + for (const hook of hooks) { + const val = await fn(hook); + if (predicate(val)) return val; + } + return []; }; // Calls the hook function synchronously and returns the value provided by the hook function (via @@ -377,15 +372,9 @@ exports.callFirst = (hookName, context) => { const aCallFirst = (hookName, context, cb, predicate) => { if (!context) context = {}; if (!cb) cb = () => {}; - if (pluginDefs.hooks[hookName] === undefined) return cb(null, []); - mapFirst( - pluginDefs.hooks[hookName], - (hook, cb) => { - hookCallWrapper(hook, hookName, context, (res) => { cb(null, res); }); - }, - cb, - predicate - ); + const hooks = pluginDefs.hooks[hookName] || []; + const fn = async (hook) => await util.promisify(hookCallWrapper)(hook, hookName, context); + util.callbackify(mapFirst)(hooks, fn, predicate, cb); }; /* return a Promise if cb is not supplied */ From 4ab7a99512a69b5438b45a7f37a2b824433f4eec Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 31 Jan 2021 18:35:17 -0500 Subject: [PATCH 17/31] hooks: Inline `syncMapFirst()` into `callFirst()` for readability There's only one caller of the function, and the function is simple, so there's no need for a separate function. --- src/static/js/pluginfw/hooks.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index de7ddc32c..4370b87f8 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -40,15 +40,6 @@ const hookCallWrapper = (hook, hookName, context, cb) => { return () => normalize(hook.hook_fn(hookName, context, (x) => cb(normalize(x)))); }; -const syncMapFirst = (hooks, fn) => { - const predicate = (val) => val.length; - for (const hook of hooks) { - const val = fn(hook); - if (predicate(val)) return val; - } - return []; -}; - const mapFirst = async (hooks, fn, predicate = null) => { if (predicate == null) predicate = (val) => val.length; for (const hook of hooks) { @@ -364,9 +355,13 @@ exports.aCallAll = async (hookName, context, cb) => { exports.callFirst = (hookName, context) => { if (!context) context = {}; - if (pluginDefs.hooks[hookName] === undefined) return []; - return syncMapFirst(pluginDefs.hooks[hookName], - (hook) => hookCallWrapper(hook, hookName, context)); + const predicate = (val) => val.length; + const hooks = pluginDefs.hooks[hookName] || []; + for (const hook of hooks) { + const val = hookCallWrapper(hook, hookName, context); + if (predicate(val)) return val; + } + return []; }; const aCallFirst = (hookName, context, cb, predicate) => { From 13e806ad7a21e18352880b16b43b1caa2784887f Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 31 Jan 2021 18:35:17 -0500 Subject: [PATCH 18/31] hooks: Inline `mapFirst()` into `aCallFirst()` for readability There's only one caller of the function, and the function is simple, so there's no need for a separate function. --- src/static/js/pluginfw/hooks.js | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 4370b87f8..2851e92a0 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -40,15 +40,6 @@ const hookCallWrapper = (hook, hookName, context, cb) => { return () => normalize(hook.hook_fn(hookName, context, (x) => cb(normalize(x)))); }; -const mapFirst = async (hooks, fn, predicate = null) => { - if (predicate == null) predicate = (val) => val.length; - for (const hook of hooks) { - const val = await fn(hook); - if (predicate(val)) return val; - } - return []; -}; - // Calls the hook function synchronously and returns the value provided by the hook function (via // callback or return value). // @@ -364,12 +355,18 @@ exports.callFirst = (hookName, context) => { return []; }; -const aCallFirst = (hookName, context, cb, predicate) => { +const aCallFirst = (hookName, context, cb, predicate = null) => { if (!context) context = {}; if (!cb) cb = () => {}; + if (predicate == null) predicate = (val) => val.length; const hooks = pluginDefs.hooks[hookName] || []; - const fn = async (hook) => await util.promisify(hookCallWrapper)(hook, hookName, context); - util.callbackify(mapFirst)(hooks, fn, predicate, cb); + util.callbackify(async () => { + for (const hook of hooks) { + const val = await util.promisify(hookCallWrapper)(hook, hookName, context); + if (predicate(val)) return val; + } + return []; + })(cb); }; /* return a Promise if cb is not supplied */ From 708206449a888ed46a0acdf52de68d996778696d Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 31 Jan 2021 19:11:48 -0500 Subject: [PATCH 19/31] hooks: Factor out callback attachment The separate function will be reused in a future commit. --- src/static/js/pluginfw/hooks.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 2851e92a0..2c0dd13b7 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -24,6 +24,11 @@ const checkDeprecation = (hook) => { deprecationWarned[hook.hook_fn_name] = true; }; +// Calls the node-style callback when the Promise settles. Unlike util.callbackify, this takes a +// Promise (rather than a function that returns a Promise), and it returns a Promise (rather than a +// function that returns undefined). +const attachCallback = (p, cb) => p.then((val) => cb(null, val), cb); + // Flattens the array one level. const flatten1 = (array) => array.reduce((a, b) => a.concat(b), []); @@ -333,15 +338,14 @@ const callHookFnAsync = async (hook, context) => { // 2. Convert each `undefined` entry into `[]`. // 3. Flatten one level. // If cb is non-null, this function resolves to the value returned by cb. -exports.aCallAll = async (hookName, context, cb) => { +exports.aCallAll = async (hookName, context, cb = null) => { + if (cb != null) return await attachCallback(exports.aCallAll(hookName, context), cb); if (context == null) context = {}; const hooks = pluginDefs.hooks[hookName] || []; - let resultsPromise = Promise.all(hooks.map((hook) => callHookFnAsync(hook, context) + const results = await Promise.all(hooks.map((hook) => callHookFnAsync(hook, context) // `undefined` (but not `null`!) is treated the same as []. - .then((result) => (result === undefined) ? [] : result))) - .then(flatten1); - if (cb != null) resultsPromise = resultsPromise.then((val) => cb(null, val), cb); - return await resultsPromise; + .then((result) => (result === undefined) ? [] : result))); + return flatten1(results); }; exports.callFirst = (hookName, context) => { From f316a3bacdc282de9cd60121013ea6421bfab9ca Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 31 Jan 2021 23:41:11 -0500 Subject: [PATCH 20/31] hooks: Never pass a falsy error to a callback --- src/static/js/pluginfw/hooks.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 2c0dd13b7..1b04a61c7 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -27,7 +27,11 @@ const checkDeprecation = (hook) => { // Calls the node-style callback when the Promise settles. Unlike util.callbackify, this takes a // Promise (rather than a function that returns a Promise), and it returns a Promise (rather than a // function that returns undefined). -const attachCallback = (p, cb) => p.then((val) => cb(null, val), cb); +const attachCallback = (p, cb) => p.then( + (val) => cb(null, val), + // Callbacks often only check the truthiness, not the nullness, of the first parameter. To avoid + // problems, always pass a truthy value as the first argument if the Promise is rejected. + (err) => cb(err || new Error(err))); // Flattens the array one level. const flatten1 = (array) => array.reduce((a, b) => a.concat(b), []); @@ -326,10 +330,11 @@ const callHookFnAsync = async (hook, context) => { // Arguments: // * hookName: Name of the hook to invoke. // * context: Passed unmodified to the hook functions, except nullish becomes {}. -// * cb: Deprecated callback. The following: +// * cb: Deprecated. Optional node-style callback. The following: // const p1 = hooks.aCallAll('myHook', context, cb); // is equivalent to: -// const p2 = hooks.aCallAll('myHook', context).then((val) => cb(null, val), cb); +// const p2 = hooks.aCallAll('myHook', context).then( +// (val) => cb(null, val), (err) => cb(err || new Error(err))); // // Return value: // If cb is nullish, this function resolves to a flattened array of hook results. Specifically, it From 22d02dbcbf3da3e8373f52cb99572144e1bd2e98 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Sun, 31 Jan 2021 18:56:06 -0500 Subject: [PATCH 21/31] hooks: Factor out value normalization --- src/static/js/pluginfw/hooks.js | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 1b04a61c7..5b3add2d1 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -33,20 +33,24 @@ const attachCallback = (p, cb) => p.then( // 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]; +}; + // Flattens the array one level. const flatten1 = (array) => array.reduce((a, b) => a.concat(b), []); const hookCallWrapper = (hook, hookName, context, cb) => { if (cb === undefined) cb = (x) => x; - checkDeprecation(hook); - - // Normalize output to list for both sync and async cases - const normalize = (x) => { - if (x === undefined) return []; - return x; - }; - return () => normalize(hook.hook_fn(hookName, context, (x) => cb(normalize(x)))); + return () => normalizeValue(hook.hook_fn(hookName, context, (x) => cb(normalizeValue(x)))); }; // Calls the hook function synchronously and returns the value provided by the hook function (via @@ -192,12 +196,7 @@ const callHookFnSync = (hook, context) => { exports.callAll = (hookName, context) => { if (context == null) context = {}; const hooks = pluginDefs.hooks[hookName] || []; - return flatten1(hooks.map((hook) => { - const ret = callHookFnSync(hook, context); - // `undefined` (but not `null`!) is treated the same as []. - if (ret === undefined) return []; - return ret; - })); + return flatten1(hooks.map((hook) => normalizeValue(callHookFnSync(hook, context)))); }; // Calls the hook function asynchronously and returns a Promise that either resolves to the hook @@ -347,9 +346,8 @@ exports.aCallAll = async (hookName, context, cb = null) => { if (cb != null) return await attachCallback(exports.aCallAll(hookName, context), cb); if (context == null) context = {}; const hooks = pluginDefs.hooks[hookName] || []; - const results = await Promise.all(hooks.map((hook) => callHookFnAsync(hook, context) - // `undefined` (but not `null`!) is treated the same as []. - .then((result) => (result === undefined) ? [] : result))); + const results = await Promise.all( + hooks.map(async (hook) => normalizeValue(await callHookFnAsync(hook, context)))); return flatten1(results); }; From 77f480d954bcefd99c176e87604d47aa9b8848ee Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 1 Feb 2021 00:03:06 -0500 Subject: [PATCH 22/31] hooks: Asyncify `aCallFirst` --- src/static/js/pluginfw/hooks.js | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 5b3add2d1..c570068cf 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -362,29 +362,21 @@ exports.callFirst = (hookName, context) => { return []; }; -const aCallFirst = (hookName, context, cb, predicate = null) => { +const aCallFirst = async (hookName, context, predicate = null) => { if (!context) context = {}; - if (!cb) cb = () => {}; if (predicate == null) predicate = (val) => val.length; const hooks = pluginDefs.hooks[hookName] || []; - util.callbackify(async () => { - for (const hook of hooks) { - const val = await util.promisify(hookCallWrapper)(hook, hookName, context); - if (predicate(val)) return val; - } - return []; - })(cb); + for (const hook of hooks) { + const val = await util.promisify(hookCallWrapper)(hook, hookName, context); + if (predicate(val)) return val; + } + return []; }; /* return a Promise if cb is not supplied */ exports.aCallFirst = (hookName, context, cb, predicate) => { - if (cb === undefined) { - return new Promise((resolve, reject) => { - aCallFirst(hookName, context, (err, res) => err ? reject(err) : resolve(res), predicate); - }); - } else { - return aCallFirst(hookName, context, cb, predicate); - } + if (cb == null) return aCallFirst(hookName, context, predicate); + util.callbackify(aCallFirst)(hookName, context, predicate, cb); }; exports.exportedForTestingOnly = { From fd5d3ce777c5cb93e3c49cccc77289099afa2b71 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 1 Feb 2021 00:34:49 -0500 Subject: [PATCH 23/31] hooks: Inline `aCallFirst()` into `exports.aCallFirst()` --- src/static/js/pluginfw/hooks.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index c570068cf..75e37315f 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -362,7 +362,10 @@ exports.callFirst = (hookName, context) => { return []; }; -const aCallFirst = async (hookName, context, predicate = null) => { +exports.aCallFirst = async (hookName, context, cb = null, predicate = null) => { + if (cb != null) { + return await attachCallback(exports.aCallFirst(hookName, context, null, predicate), cb); + } if (!context) context = {}; if (predicate == null) predicate = (val) => val.length; const hooks = pluginDefs.hooks[hookName] || []; @@ -373,12 +376,6 @@ const aCallFirst = async (hookName, context, predicate = null) => { return []; }; -/* return a Promise if cb is not supplied */ -exports.aCallFirst = (hookName, context, cb, predicate) => { - if (cb == null) return aCallFirst(hookName, context, predicate); - util.callbackify(aCallFirst)(hookName, context, predicate, cb); -}; - exports.exportedForTestingOnly = { callHookFnAsync, callHookFnSync, From c11d60c5f6f2e36f1643f57ed789a74ea4224175 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 1 Feb 2021 00:37:45 -0500 Subject: [PATCH 24/31] hooks: Check context nullness, not truthiness --- src/static/js/pluginfw/hooks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 75e37315f..14bd9d57e 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -352,7 +352,7 @@ exports.aCallAll = async (hookName, context, cb = null) => { }; exports.callFirst = (hookName, context) => { - if (!context) context = {}; + if (context == null) context = {}; const predicate = (val) => val.length; const hooks = pluginDefs.hooks[hookName] || []; for (const hook of hooks) { @@ -366,7 +366,7 @@ exports.aCallFirst = async (hookName, context, cb = null, predicate = null) => { if (cb != null) { return await attachCallback(exports.aCallFirst(hookName, context, null, predicate), cb); } - if (!context) context = {}; + if (context == null) context = {}; if (predicate == null) predicate = (val) => val.length; const hooks = pluginDefs.hooks[hookName] || []; for (const hook of hooks) { From 6f30ea7c38c81b0b334426fea87462d1e23c3369 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 1 Feb 2021 00:41:53 -0500 Subject: [PATCH 25/31] hooks: Use `callHookFn{Sync,Async}()` for `{call,aCall}First()` Benefits of `callHookFnSync()` and `callHookFnAsync()`: * They are a lot more forgiving than `hookCallWrapper()` was. * They perform useful sanity checks. * They have extensive unit test coverage. * They make the behavior of `callFirst()` and `aCallFirst()` match the behavior of `callAll()` and `aCallAll()`. --- src/static/js/pluginfw/hooks.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 14bd9d57e..65854730c 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -1,7 +1,6 @@ 'use strict'; const pluginDefs = require('./plugin_defs'); -const util = require('util'); // Maps the name of a server-side hook to a string explaining the deprecation // (e.g., 'use the foo hook instead'). @@ -47,12 +46,6 @@ const normalizeValue = (val) => { // Flattens the array one level. const flatten1 = (array) => array.reduce((a, b) => a.concat(b), []); -const hookCallWrapper = (hook, hookName, context, cb) => { - if (cb === undefined) cb = (x) => x; - checkDeprecation(hook); - return () => normalizeValue(hook.hook_fn(hookName, context, (x) => cb(normalizeValue(x)))); -}; - // Calls the hook function synchronously and returns the value provided by the hook function (via // callback or return value). // @@ -356,7 +349,7 @@ exports.callFirst = (hookName, context) => { const predicate = (val) => val.length; const hooks = pluginDefs.hooks[hookName] || []; for (const hook of hooks) { - const val = hookCallWrapper(hook, hookName, context); + const val = normalizeValue(callHookFnSync(hook, context)); if (predicate(val)) return val; } return []; @@ -370,7 +363,7 @@ exports.aCallFirst = async (hookName, context, cb = null, predicate = null) => { if (predicate == null) predicate = (val) => val.length; const hooks = pluginDefs.hooks[hookName] || []; for (const hook of hooks) { - const val = await util.promisify(hookCallWrapper)(hook, hookName, context); + const val = normalizeValue(await callHookFnAsync(hook, context)); if (predicate(val)) return val; } return []; From ba0544ea9e8b443326830adc69a14103a4eda980 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 1 Feb 2021 18:31:50 -0500 Subject: [PATCH 26/31] hooks: Add unit tests for `callFirst()`, `aCallFirst()` --- tests/backend/specs/hooks.js | 240 +++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) diff --git a/tests/backend/specs/hooks.js b/tests/backend/specs/hooks.js index 327b9bd1c..2ea7fac00 100644 --- a/tests/backend/specs/hooks.js +++ b/tests/backend/specs/hooks.js @@ -389,6 +389,90 @@ describe(__filename, function () { }); }); + describe('hooks.callFirst', function () { + it('no registered hooks (undefined) -> []', async function () { + delete plugins.hooks.testHook; + assert.deepEqual(hooks.callFirst(hookName), []); + }); + + it('no registered hooks (empty list) -> []', async function () { + testHooks.length = 0; + assert.deepEqual(hooks.callFirst(hookName), []); + }); + + it('passes hook name => {}', async function () { + hook.hook_fn = (hn) => { assert.equal(hn, hookName); }; + hooks.callFirst(hookName); + }); + + it('undefined context => {}', async function () { + hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hooks.callFirst(hookName); + }); + + it('null context => {}', async function () { + hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); }; + hooks.callFirst(hookName, null); + }); + + it('context unmodified', async function () { + const wantContext = {}; + hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); }; + hooks.callFirst(hookName, wantContext); + }); + + it('predicate never satisfied -> calls all in order', async function () { + const gotCalls = []; + testHooks.length = 0; + for (let i = 0; i < 3; i++) { + const hook = makeHook(); + hook.hook_fn = () => { gotCalls.push(i); }; + testHooks.push(hook); + } + assert.deepEqual(hooks.callFirst(hookName), []); + assert.deepEqual(gotCalls, [0, 1, 2]); + }); + + it('stops when predicate is satisfied', async function () { + testHooks.length = 0; + testHooks.push(makeHook(), makeHook('val1'), makeHook('val2')); + assert.deepEqual(hooks.callFirst(hookName), ['val1']); + }); + + it('skips values that do not satisfy predicate (undefined)', async function () { + testHooks.length = 0; + testHooks.push(makeHook(), makeHook('val1')); + assert.deepEqual(hooks.callFirst(hookName), ['val1']); + }); + + it('skips values that do not satisfy predicate (empty list)', async function () { + testHooks.length = 0; + testHooks.push(makeHook([]), makeHook('val1')); + assert.deepEqual(hooks.callFirst(hookName), ['val1']); + }); + + it('null satisifes the predicate', async function () { + testHooks.length = 0; + testHooks.push(makeHook(null), makeHook('val1')); + assert.deepEqual(hooks.callFirst(hookName), [null]); + }); + + it('non-empty arrays are returned unmodified', async function () { + const want = ['val1']; + testHooks.length = 0; + testHooks.push(makeHook(want), makeHook(['val2'])); + assert.equal(hooks.callFirst(hookName), want); // Note: *NOT* deepEqual! + }); + + it('value can be passed via callback', async function () { + const want = {}; + hook.hook_fn = (hn, ctx, cb) => { cb(want); }; + const got = hooks.callFirst(hookName); + assert.deepEqual(got, [want]); + assert.equal(got[0], want); // Note: *NOT* deepEqual! + }); + }); + describe('callHookFnAsync', function () { const callHookFnAsync = hooks.exportedForTestingOnly.callHookFnAsync; // Convenience shorthand. @@ -883,4 +967,160 @@ describe(__filename, function () { }); }); }); + + 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! + }); + }); }); From 763fe6fc2615962d54ccbf80d05df49b751a90c5 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 1 Feb 2021 01:43:04 -0500 Subject: [PATCH 27/31] hooks: Document `callFirst()` and `aCallFirst()` --- src/static/js/pluginfw/hooks.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 65854730c..459f84489 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -344,6 +344,9 @@ exports.aCallAll = async (hookName, context, cb = null) => { return flatten1(results); }; +// DEPRECATED: Use `aCallFirst()` instead. +// +// Like `aCallFirst()`, but synchronous. Hook functions must provide their values synchronously. exports.callFirst = (hookName, context) => { if (context == null) context = {}; const predicate = (val) => val.length; @@ -355,6 +358,27 @@ exports.callFirst = (hookName, context) => { return []; }; +// Invokes the registered hook functions one at a time until one provides a value that meets a +// customizable condition. +// +// Arguments: +// * hookName: Name of the hook to invoke. +// * context: Passed unmodified to the hook functions, except nullish becomes {}. +// * cb: Deprecated callback. The following: +// const p1 = hooks.aCallFirst('myHook', context, cb); +// is equivalent to: +// const p2 = hooks.aCallFirst('myHook', context).then( +// (val) => cb(null, val), (err) => cb(err || new Error(err))); +// * predicate: Optional predicate function that returns true if the hook function provided a +// value that satisfies a desired condition. If nullish, the predicate defaults to a non-empty +// array check. The predicate is invoked each time a hook function returns. It takes one +// argument: the normalized value provided by the hook function. If the predicate returns +// truthy, iteration over the hook functions stops (no more hook functions will be called). +// +// Return value: +// If cb is nullish, resolves to an array that is either the normalized value that satisfied the +// predicate or empty if the predicate was never satisfied. If cb is non-nullish, resolves to the +// value returned from cb(). exports.aCallFirst = async (hookName, context, cb = null, predicate = null) => { if (cb != null) { return await attachCallback(exports.aCallFirst(hookName, context, null, predicate), cb); From 05e0e8dbf7941cb2ff4b235a088034b2404fce46 Mon Sep 17 00:00:00 2001 From: Richard Hansen Date: Mon, 1 Feb 2021 01:18:44 -0500 Subject: [PATCH 28/31] hooks: New `callAllSerial()` function This is necessary to migrate away from `callAll()` (which only supports synchronous hook functions). --- src/static/js/pluginfw/hooks.js | 20 +++++++- tests/backend/specs/hooks.js | 83 +++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/src/static/js/pluginfw/hooks.js b/src/static/js/pluginfw/hooks.js index 459f84489..72da63021 100644 --- a/src/static/js/pluginfw/hooks.js +++ b/src/static/js/pluginfw/hooks.js @@ -175,6 +175,8 @@ const callHookFnSync = (hook, context) => { return outcome.val; }; +// DEPRECATED: Use `callAllSerial()` or `aCallAll()` instead. +// // Invokes all registered hook functions synchronously. // // Arguments: @@ -317,7 +319,10 @@ const callHookFnAsync = async (hook, context) => { }); }; -// Invokes all registered hook functions asynchronously. +// Invokes all registered hook functions asynchronously and concurrently. This is NOT the async +// equivalent of `callAll()`: `callAll()` calls the hook functions serially (one at a time) but this +// function calls them concurrently. Use `callAllSerial()` if the hook functions must be called one +// at a time. // // Arguments: // * hookName: Name of the hook to invoke. @@ -344,6 +349,19 @@ exports.aCallAll = async (hookName, context, cb = null) => { return flatten1(results); }; +// Like `aCallAll()` except the hook functions are called one at a time instead of concurrently. +// Only use this function if the hook functions must be called one at a time, otherwise use +// `aCallAll()`. +exports.callAllSerial = async (hookName, context) => { + if (context == null) context = {}; + const hooks = pluginDefs.hooks[hookName] || []; + const results = []; + for (const hook of hooks) { + results.push(normalizeValue(await callHookFnAsync(hook, context))); + } + return flatten1(results); +}; + // DEPRECATED: Use `aCallFirst()` instead. // // Like `aCallFirst()`, but synchronous. Hook functions must provide their values synchronously. diff --git a/tests/backend/specs/hooks.js b/tests/backend/specs/hooks.js index 2ea7fac00..865cb132b 100644 --- a/tests/backend/specs/hooks.js +++ b/tests/backend/specs/hooks.js @@ -968,6 +968,89 @@ 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; From 65dec5bd2cfd3576fbfe6de5e5d652888a999e3f Mon Sep 17 00:00:00 2001 From: John McLear Date: Mon, 1 Feb 2021 23:11:11 +0000 Subject: [PATCH 29/31] lint: json.js --- bin/doc/json.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/bin/doc/json.js b/bin/doc/json.js index c71611e5f..1a5ecb1d8 100644 --- a/bin/doc/json.js +++ b/bin/doc/json.js @@ -248,11 +248,11 @@ const processList = (section) => { switch (section.type) { case 'ctor': case 'classMethod': - case 'method': + case 'method': { // each item is an argument, unless the name is 'return', // in which case it's the return value. section.signatures = section.signatures || []; - var sig = {}; + const sig = {}; section.signatures.push(sig); sig.params = values.filter((v) => { if (v.name === 'return') { @@ -263,11 +263,11 @@ const processList = (section) => { }); parseSignature(section.textRaw, sig); break; - - case 'property': + } + case 'property': { // there should be only one item, which is the value. // copy the data up to the section. - var value = values[0] || {}; + const value = values[0] || {}; delete value.name; section.typeof = value.type; delete value.type; @@ -275,14 +275,15 @@ const processList = (section) => { section[k] = value[k]; }); break; + } - case 'event': + case 'event': { // event: each item is an argument. section.params = values; break; + } } - // section.listParsed = values; delete section.list; }; @@ -417,7 +418,7 @@ const finishSection = (section, parent) => { ctor.signatures.forEach((sig) => { sig.desc = ctor.desc; }); - sigs.push.apply(sigs, ctor.signatures); + sigs.push(...ctor.signatures); }); delete section.ctors; } @@ -486,7 +487,7 @@ const finishSection = (section, parent) => { // Not a general purpose deep copy. // But sufficient for these basic things. 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]); }); }; From ea202e41f69fcb88e8f3f34c8dba7de45cb78865 Mon Sep 17 00:00:00 2001 From: freddii Date: Wed, 3 Feb 2021 00:30:07 +0100 Subject: [PATCH 30/31] docs: fixed typos --- CHANGELOG.md | 16 ++++++++-------- bin/checkPadDeltas.js | 2 +- bin/cleanRun.sh | 2 +- bin/debugRun.sh | 2 +- bin/fastRun.sh | 2 +- bin/installDeps.sh | 2 +- bin/repairPad.js | 2 +- bin/run.sh | 2 +- doc/api/http_api.md | 2 +- doc/localization.md | 2 +- settings.json.docker | 2 +- settings.json.template | 2 +- src/node/db/DB.js | 4 ++-- src/node/db/PadManager.js | 2 +- src/node/handler/PadMessageHandler.js | 4 ++-- src/node/hooks/express/openapi.js | 2 +- src/node/hooks/express/socketio.js | 2 +- src/node/utils/Minify.js | 4 ++-- src/static/js/pad.js | 2 +- tests/backend/fuzzImportTest.js | 2 +- tests/backend/specs/api/importexport.js | 4 ++-- tests/backend/specs/api/pad.js | 4 ++-- tests/backend/specs/contentcollector.js | 4 ++-- tests/frontend/runner.js | 2 +- 24 files changed, 37 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42c2692b6..57613b826 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -122,7 +122,7 @@ * MINOR: Fix ?showChat URL param issue * MINOR: Issue where timeslider URI fails to be correct if padID is numeric * MINOR: Include prompt for clear authorship when entire document is selected -* MINOR: Include full document aText every 100 revisions to make pad restoration on database curruption achievable +* MINOR: Include full document aText every 100 revisions to make pad restoration on database corruption achievable * MINOR: Several Colibris CSS fixes * MINOR: Use mime library for mime types instead of hard-coded. * MINOR: Don't show "new pad button" if instance is read only @@ -372,7 +372,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. # 1.5.3 * NEW: Accessibility support for Screen readers, includes new fonts and keyboard shortcuts * NEW: API endpoint for Append Chat Message and Chat Backend Tests - * NEW: Error messages displayed on load are included in Default Pad Text (can be supressed) + * NEW: Error messages displayed on load are included in Default Pad Text (can be suppressed) * NEW: Content Collector can handle key values * NEW: getAttributesOnPosition Method * FIX: Firefox keeps attributes (bold etc) on cut/copy -> paste @@ -441,7 +441,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. * Fix: Timeslider UI Fix * Fix: Remove Dokuwiki * Fix: Remove long paths from windows build (stops error during extract) - * Fix: Various globals remvoed + * Fix: Various globals removed * Fix: Move all scripts into bin/ * Fix: Various CSS bugfixes for Mobile devices * Fix: Overflow Toolbar @@ -517,7 +517,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. * FIX: HTML import (don't crash on malformed or blank HTML input; strip title out of html during import) * FIX: check if uploaded file only contains ascii chars when abiword disabled * FIX: Plugin search in /admin/plugins - * FIX: Don't create new pad if a non-existant read-only pad is accessed + * FIX: Don't create new pad if a non-existent read-only pad is accessed * FIX: Drop messages from unknown connections (would lead to a crash after a restart) * FIX: API: fix createGroupFor endpoint, if mapped group is deleted * FIX: Import form for other locales @@ -534,7 +534,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. * NEW: Bump log4js for improved logging * Fix: Remove URL schemes which don't have RFC standard * Fix: Fix safeRun subsequent restarts issue - * Fix: Allow safeRun to pass arguements to run.sh + * Fix: Allow safeRun to pass arguments to run.sh * Fix: Include script for more efficient import * Fix: Fix sysv comptibile script * Fix: Fix client side changeset spamming @@ -573,7 +573,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. * Fix: Support Node 0.10 * Fix: Log HTTP on DEBUG log level * Fix: Server wont crash on import fails on 0 file import. - * Fix: Import no longer fails consistantly + * Fix: Import no longer fails consistently * Fix: Language support for non existing languages * Fix: Mobile support for chat notifications are now usable * Fix: Re-Enable Editbar buttons on reconnect @@ -605,7 +605,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. * NEW: Admin dashboard mobile device support and new hooks for Admin dashboard * NEW: Get current API version from API * NEW: CLI script to delete pads - * Fix: Automatic client reconnection on disonnect + * Fix: Automatic client reconnection on disconnect * Fix: Text Export indentation now supports multiple indentations * Fix: Bugfix getChatHistory API method * Fix: Stop Chrome losing caret after paste is texted @@ -625,7 +625,7 @@ finally put them back in their new location, uder `src/static/skins/no-skin`. * Fix: Stop Opera browser inserting two new lines on enter keypress * Fix: Stop timeslider from showing NaN on pads with only one revision * Other: Allow timeslider tests to run and provide & fix various other frontend-tests - * Other: Begin dropping referene to Lite. Etherpad Lite is now named "Etherpad" + * Other: Begin dropping reference to Lite. Etherpad Lite is now named "Etherpad" * Other: Update to latest jQuery * Other: Change loading message asking user to please wait on first build * Other: Allow etherpad to use global npm installation (Safe since node 6.3) diff --git a/bin/checkPadDeltas.js b/bin/checkPadDeltas.js index 45ee27d72..4900595c3 100644 --- a/bin/checkPadDeltas.js +++ b/bin/checkPadDeltas.js @@ -51,7 +51,7 @@ const util = require('util'); let atext = Changeset.makeAText('\n'); - // run trough all revisions + // run through all revisions for (const revNum of revisions) { // console.log('Fetching', revNum) const revision = await db.get(`pad:${padId}:revs:${revNum}`); diff --git a/bin/cleanRun.sh b/bin/cleanRun.sh index 57de27e5c..36dd1e384 100755 --- a/bin/cleanRun.sh +++ b/bin/cleanRun.sh @@ -3,7 +3,7 @@ # Move to the folder where ep-lite is installed cd "$(dirname "$0")"/.. -# Source constants and usefull functions +# Source constants and useful functions . bin/functions.sh #Was this script started in the bin folder? if yes move out diff --git a/bin/debugRun.sh b/bin/debugRun.sh index 9b2fff9bd..e04db88d0 100755 --- a/bin/debugRun.sh +++ b/bin/debugRun.sh @@ -3,7 +3,7 @@ # Move to the folder where ep-lite is installed cd "$(dirname "$0")"/.. -# Source constants and usefull functions +# Source constants and useful functions . bin/functions.sh # Prepare the environment diff --git a/bin/fastRun.sh b/bin/fastRun.sh index 90d83dc8e..63524e793 100755 --- a/bin/fastRun.sh +++ b/bin/fastRun.sh @@ -12,7 +12,7 @@ set -eu # source: https://stackoverflow.com/questions/59895/how-to-get-the-source-directory-of-a-bash-script-from-within-the-script-itself#246128 DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -# Source constants and usefull functions +# Source constants and useful functions . ${DIR}/../bin/functions.sh echo "Running directly, without checking/installing dependencies" diff --git a/bin/installDeps.sh b/bin/installDeps.sh index bdce38fc7..be3f1fd8f 100755 --- a/bin/installDeps.sh +++ b/bin/installDeps.sh @@ -3,7 +3,7 @@ # Move to the folder where ep-lite is installed cd "$(dirname "$0")"/.. -# Source constants and usefull functions +# Source constants and useful functions . bin/functions.sh # Is node installed? diff --git a/bin/repairPad.js b/bin/repairPad.js index ff2da9776..ce0c6072e 100644 --- a/bin/repairPad.js +++ b/bin/repairPad.js @@ -23,7 +23,7 @@ const util = require('util'); (async () => { await util.promisify(npm.load)({}); - // intialize database + // initialize database require('ep_etherpad-lite/node/utils/Settings'); const db = require('ep_etherpad-lite/node/db/DB'); await db.init(); diff --git a/bin/run.sh b/bin/run.sh index 50bce4bdd..c10359904 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -3,7 +3,7 @@ # Move to the folder where ep-lite is installed cd "$(dirname "$0")"/.. -# Source constants and usefull functions +# Source constants and useful functions . bin/functions.sh ignoreRoot=0 diff --git a/doc/api/http_api.md b/doc/api/http_api.md index fb570a393..0cfc85a07 100644 --- a/doc/api/http_api.md +++ b/doc/api/http_api.md @@ -263,7 +263,7 @@ deletes a session #### getSessionInfo(sessionID) * API >= 1 -returns informations about a session +returns information about a session *Example returns:* * `{code: 0, message:"ok", data: {authorID: "a.s8oes9dhwrvt0zif", groupID: g.s8oes9dhwrvt0zif, validUntil: 1312201246}}` diff --git a/doc/localization.md b/doc/localization.md index 54675e2da..d047944ff 100644 --- a/doc/localization.md +++ b/doc/localization.md @@ -95,7 +95,7 @@ For example, if you want to replace `Chat` with `Notes`, simply add... ## Customization for Administrators -As an Etherpad administrator, it is possible to overwrite core mesages as well as messages in plugins. These include error messages, labels, and user instructions. Whereas the localization in the source code is in separate files separated by locale, an administrator's custom localizations are in `settings.json` under the `customLocaleStrings` key, with each locale separated by a sub-key underneath. +As an Etherpad administrator, it is possible to overwrite core messages as well as messages in plugins. These include error messages, labels, and user instructions. Whereas the localization in the source code is in separate files separated by locale, an administrator's custom localizations are in `settings.json` under the `customLocaleStrings` key, with each locale separated by a sub-key underneath. For example, let's say you want to change the text on the "New Pad" button on Etherpad's home page. If you look in `locales/en.json` (or `locales/en-gb.json`) you'll see the key for this text is `"index.newPad"`. You could add the following to `settings.json`: diff --git a/settings.json.docker b/settings.json.docker index 081af38b8..6938b2713 100644 --- a/settings.json.docker +++ b/settings.json.docker @@ -171,7 +171,7 @@ * * * Database specific settings are dependent on dbType, and go in dbSettings. - * Remember that since Etherpad 1.6.0 you can also store these informations in + * Remember that since Etherpad 1.6.0 you can also store this information in * credentials.json. * * For a complete list of the supported drivers, please refer to: diff --git a/settings.json.template b/settings.json.template index f4cfdea62..3c9c50837 100644 --- a/settings.json.template +++ b/settings.json.template @@ -162,7 +162,7 @@ * * * Database specific settings are dependent on dbType, and go in dbSettings. - * Remember that since Etherpad 1.6.0 you can also store these informations in + * Remember that since Etherpad 1.6.0 you can also store this information in * credentials.json. * * For a complete list of the supported drivers, please refer to: diff --git a/src/node/db/DB.js b/src/node/db/DB.js index 12d3d9f80..c0993e8ec 100644 --- a/src/node/db/DB.js +++ b/src/node/db/DB.js @@ -1,7 +1,7 @@ 'use strict'; /** - * The DB Module provides a database initalized with the settings + * The DB Module provides a database initialized with the settings * provided by the settings module */ @@ -36,7 +36,7 @@ const db = exports.db = null; /** - * Initalizes the database with the settings provided by the settings module + * Initializes the database with the settings provided by the settings module * @param {Function} callback */ exports.init = async () => await new Promise((resolve, reject) => { diff --git a/src/node/db/PadManager.js b/src/node/db/PadManager.js index 11e8cb1a5..48507949e 100644 --- a/src/node/db/PadManager.js +++ b/src/node/db/PadManager.js @@ -139,7 +139,7 @@ exports.getPad = async (id, text) => { // try to load pad pad = new Pad(id); - // initalize the pad + // initialize the pad await pad.init(text); globalPads.set(id, pad); padList.addPad(id); diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 01981d1f0..2a1f6385b 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -46,7 +46,7 @@ const rateLimiter = new RateLimiterMemory({ }); /** - * A associative array that saves informations about a session + * A associative array that saves information about a session * key = sessionId * values = padId, readonlyPadId, readonly, author, rev * padId = the real padId of the pad @@ -88,7 +88,7 @@ exports.setSocketIO = (socket_io) => { exports.handleConnect = (socket) => { stats.meter('connects').mark(); - // Initalize sessioninfos for this new session + // Initialize sessioninfos for this new session sessioninfos[socket.id] = {}; }; diff --git a/src/node/hooks/express/openapi.js b/src/node/hooks/express/openapi.js index 444077fda..950b56601 100644 --- a/src/node/hooks/express/openapi.js +++ b/src/node/hooks/express/openapi.js @@ -141,7 +141,7 @@ const resources = { // We need an operation that returns a SessionInfo so it can be picked up by the codegen :( info: { operationId: 'getSessionInfo', - summary: 'returns informations about a session', + summary: 'returns information about a session', responseSchema: {info: {$ref: '#/components/schemas/SessionInfo'}}, }, }, diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index 3d9e9debe..58d2f5a44 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -88,7 +88,7 @@ exports.expressCreateServer = (hookName, args, cb) => { // http://stackoverflow.com/questions/23981741/minify-socket-io-socket-io-js-with-1-0 // if(settings.minify) io.enable('browser client minification'); - // Initalize the Socket.IO Router + // Initialize the Socket.IO Router socketIORouter.setSocketIO(io); socketIORouter.addComponent('pad', padMessageHandler); diff --git a/src/node/utils/Minify.js b/src/node/utils/Minify.js index 66a1926a9..49d26ba6b 100644 --- a/src/node/utils/Minify.js +++ b/src/node/utils/Minify.js @@ -311,7 +311,7 @@ const statFile = (filename, callback, dirStatLimit) => { const lastModifiedDateOfEverything = (callback) => { const folders2check = [`${ROOT_DIR}js/`, `${ROOT_DIR}css/`]; let latestModification = 0; - // go trough this two folders + // go through this two folders async.forEach(folders2check, (path, callback) => { // read the files in the folder fs.readdir(path, (err, files) => { @@ -320,7 +320,7 @@ const lastModifiedDateOfEverything = (callback) => { // we wanna check the directory itself for changes too files.push('.'); - // go trough all files in this folder + // go through all files in this folder async.forEach(files, (filename, callback) => { // get the stat data of this file fs.stat(`${path}/${filename}`, (err, stats) => { diff --git a/src/static/js/pad.js b/src/static/js/pad.js index 1deee9f7e..753ab7d02 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.js @@ -294,7 +294,7 @@ const handshake = () => { // set some client vars window.clientVars = obj.data; - // initalize the pad + // initialize the pad pad._afterHandshake(); if (clientVars.readonly) { diff --git a/tests/backend/fuzzImportTest.js b/tests/backend/fuzzImportTest.js index e2667e8f9..eb5a17d17 100644 --- a/tests/backend/fuzzImportTest.js +++ b/tests/backend/fuzzImportTest.js @@ -42,7 +42,7 @@ function runTest(number) { let fN = '/test.txt'; let cT = 'text/plain'; - // To be more agressive every other test we mess with Etherpad + // To be more aggressive every other test we mess with Etherpad // We provide a weird file name and also set a weird contentType if (number % 2 == 0) { fN = froth().toString(); diff --git a/tests/backend/specs/api/importexport.js b/tests/backend/specs/api/importexport.js index 1a9e94332..1e4461530 100644 --- a/tests/backend/specs/api/importexport.js +++ b/tests/backend/specs/api/importexport.js @@ -94,7 +94,7 @@ const testImports = { wantText: ' word1 word2 word3\n\n', }, 'nonBreakingSpacePreceededBySpaceBetweenWords': { - description: 'A non-breaking space preceeded by a normal space', + description: 'A non-breaking space preceded by a normal space', input: '  word1  word2  word3
        ', wantHTML: ' word1  word2  word3

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

        1

        preline
        diff --git a/tests/backend/specs/api/pad.js b/tests/backend/specs/api/pad.js
        index 15ec4f0be..a0bbec62c 100644
        --- a/tests/backend/specs/api/pad.js
        +++ b/tests/backend/specs/api/pad.js
        @@ -102,9 +102,9 @@ describe(__filename, function () {
                                  -> setText(padId, "hello world")
                                   -> getLastEdited(padID) -- Should be when pad was made
                                    -> getText(padId) -- Should be "hello world"
        -                            -> movePad(padID, newPadId) -- Should provide consistant pad data
        +                            -> movePad(padID, newPadId) -- Should provide consistent pad data
                                      -> getText(newPadId) -- Should be "hello world"
        -                              -> movePad(newPadID, originalPadId) -- Should provide consistant pad data
        +                              -> movePad(newPadID, originalPadId) -- Should provide consistent pad data
                                        -> getText(originalPadId) -- Should be "hello world"
                                         -> getLastEdited(padID) -- Should not be 0
                                         -> appendText(padID, "hello")
        diff --git a/tests/backend/specs/contentcollector.js b/tests/backend/specs/contentcollector.js
        index e6aff389a..62bdf5d5d 100644
        --- a/tests/backend/specs/contentcollector.js
        +++ b/tests/backend/specs/contentcollector.js
        @@ -140,7 +140,7 @@ const tests = {
             wantText: ['  word1  word2   word3'],
           },
           nonBreakingSpacePreceededBySpaceBetweenWords: {
        -    description: 'A non-breaking space preceeded by a normal space',
        +    description: 'A non-breaking space preceded by a normal space',
             html: '  word1  word2  word3
        ', wantLineAttribs: ['+l'], wantText: [' word1 word2 word3'], @@ -240,7 +240,7 @@ pre ], }, preIntroducesASpace: { - description: 'pre should be on a new line not preceeded by a space', + description: 'pre should be on a new line not preceded by a space', html: `

        1

        preline
        diff --git a/tests/frontend/runner.js b/tests/frontend/runner.js
        index 616c065b3..fcc56a7d6 100644
        --- a/tests/frontend/runner.js
        +++ b/tests/frontend/runner.js
        @@ -170,7 +170,7 @@ $(() => {
             }
           });
         
        -  // initalize the test helper
        +  // initialize the test helper
           helper.init(() => {
             // configure and start the test framework
             const grep = getURLParameter('grep');
        
        From 1076783985a9d8274590a97c585a7277d19ee3cb Mon Sep 17 00:00:00 2001
        From: John McLear 
        Date: Wed, 3 Feb 2021 10:39:30 +0000
        Subject: [PATCH 31/31] tests: backend test coverage for #3227 where a group
         cannot be deleted if it has pads.
        
        ---
         tests/backend/specs/api/sessionsAndGroups.js | 77 ++++++++++++++++++++
         1 file changed, 77 insertions(+)
        
        diff --git a/tests/backend/specs/api/sessionsAndGroups.js b/tests/backend/specs/api/sessionsAndGroups.js
        index 3b5e1a912..655324722 100644
        --- a/tests/backend/specs/api/sessionsAndGroups.js
        +++ b/tests/backend/specs/api/sessionsAndGroups.js
        @@ -93,6 +93,83 @@ describe(__filename, function () {
                     assert(res.body.data.groupID);
                   });
             });
        +
        +    // Test coverage for https://github.com/ether/etherpad-lite/issues/4227
        +    // Creates a group, creates 2 sessions, 2 pads and then deletes the group.
        +    it('createGroup', async function () {
        +      await api.get(endPoint('createGroup'))
        +          .expect(200)
        +          .expect('Content-Type', /json/)
        +          .expect((res) => {
        +            assert.equal(res.body.code, 0);
        +            assert(res.body.data.groupID);
        +            groupID = res.body.data.groupID;
        +          });
        +    });
        +
        +    it('createAuthor', async function () {
        +      await api.get(endPoint('createAuthor'))
        +          .expect(200)
        +          .expect('Content-Type', /json/)
        +          .expect((res) => {
        +            assert.equal(res.body.code, 0);
        +            assert(res.body.data.authorID);
        +            authorID = res.body.data.authorID;
        +          });
        +    });
        +
        +    it('createSession', async function () {
        +      await api.get(`${endPoint('createSession')
        +      }&authorID=${authorID}&groupID=${groupID}&validUntil=999999999999`)
        +          .expect(200)
        +          .expect('Content-Type', /json/)
        +          .expect((res) => {
        +            assert.equal(res.body.code, 0);
        +            assert(res.body.data.sessionID);
        +            sessionID = res.body.data.sessionID;
        +          });
        +    });
        +
        +    it('createSession', async function () {
        +      await api.get(`${endPoint('createSession')
        +      }&authorID=${authorID}&groupID=${groupID}&validUntil=999999999999`)
        +          .expect(200)
        +          .expect('Content-Type', /json/)
        +          .expect((res) => {
        +            assert.equal(res.body.code, 0);
        +            assert(res.body.data.sessionID);
        +            sessionID = res.body.data.sessionID;
        +          });
        +    });
        +
        +    it('createGroupPad', async function () {
        +      await api.get(`${endPoint('createGroupPad')}&groupID=${groupID}&padName=x1234567`)
        +          .expect(200)
        +          .expect('Content-Type', /json/)
        +          .expect((res) => {
        +            assert.equal(res.body.code, 0);
        +          });
        +    });
        +
        +    it('createGroupPad', async function () {
        +      await api.get(`${endPoint('createGroupPad')}&groupID=${groupID}&padName=x12345678`)
        +          .expect(200)
        +          .expect('Content-Type', /json/)
        +          .expect((res) => {
        +            assert.equal(res.body.code, 0);
        +          });
        +    });
        +
        +    it('deleteGroup', async function () {
        +      await api.get(`${endPoint('deleteGroup')}&groupID=${groupID}`)
        +          .expect(200)
        +          .expect('Content-Type', /json/)
        +          .expect((res) => {
        +            assert.equal(res.body.code, 0);
        +          });
        +    });
        +    // End of coverage for https://github.com/ether/etherpad-lite/issues/4227
        +
           });
         
           describe('API: Author creation', function () {