diff --git a/CHANGELOG.md b/CHANGELOG.md
index 28bc0d2d8..093150284 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,15 @@
# Develop -- TODO Change to 1.8.x.
* ...
+# 1.8.6
+* IMPORTANT: This fixes a severe problem with postgresql in 1.8.5
+* SECURITY: Fix authentication and authorization bypass vulnerabilities
+* API: Update version to 1.2.15
+* FEATURE: Add copyPadWithoutHistory API (#4295)
+* FEATURE: Package more asset files to save http requests (#4286)
+* MINOR: Improve UI when reconnecting
+* TESTS: Improve tests
+
# 1.8.5
* IMPORTANT DROP OF SUPPORT: Drop support for IE. Browsers now need async/await.
* IMPORTANT SECURITY: Rate limit Commits when env=production
diff --git a/src/locales/de.json b/src/locales/de.json
index bcee159f2..7be360596 100644
--- a/src/locales/de.json
+++ b/src/locales/de.json
@@ -51,7 +51,7 @@
"pad.settings.fontType.normal": "Normal",
"pad.settings.language": "Sprache:",
"pad.settings.about": "Über",
- "pad.settings.poweredBy": "Powered by $1",
+ "pad.settings.poweredBy": "Powered by",
"pad.importExport.import_export": "Import/Export",
"pad.importExport.import": "Textdatei oder Dokument hochladen",
"pad.importExport.importSuccessful": "Erfolgreich!",
diff --git a/src/locales/is.json b/src/locales/is.json
index 0d7fd5afa..394503f1a 100644
--- a/src/locales/is.json
+++ b/src/locales/is.json
@@ -41,6 +41,8 @@
"pad.settings.fontType": "Leturgerð:",
"pad.settings.fontType.normal": "Venjulegt",
"pad.settings.language": "Tungumál:",
+ "pad.settings.about": "Um hugbúnaðinn",
+ "pad.settings.poweredBy": "Keyrt með",
"pad.importExport.import_export": "Flytja inn/út",
"pad.importExport.import": "Settu inn hverskyns texta eða skjal",
"pad.importExport.importSuccessful": "Heppnaðist!",
@@ -51,7 +53,7 @@
"pad.importExport.exportword": "Microsoft Word",
"pad.importExport.exportpdf": "PDF",
"pad.importExport.exportopen": "ODF (Open Document Format)",
- "pad.importExport.abiword.innerHTML": "Þú getur aðeins flutt inn úr hreinum texta eða HTML sniðum. Til að geta nýtt \nfleiri þróaðri innflutningssnið settu þá upp AbiWord forritið.",
+ "pad.importExport.abiword.innerHTML": "Þú getur aðeins flutt inn úr hreinum texta eða HTML sniðum. Til að geta nýtt \nfleiri þróaðri innflutningssnið settu þá upp AbiWord forritið eða LibreOffice.",
"pad.modals.connected": "Tengt.",
"pad.modals.reconnecting": "Endurtengist skrifblokkinni þinni...",
"pad.modals.forcereconnect": "Þvinga endurtengingu",
@@ -119,7 +121,7 @@
"pad.userlist.guest": "Gestur",
"pad.userlist.deny": "Hafna",
"pad.userlist.approve": "Samþykkja",
- "pad.editbar.clearcolors": "Hreinsa liti höfunda á öllu skjalinu?",
+ "pad.editbar.clearcolors": "Hreinsa liti höfunda á öllu skjalinu? Þetta er ekki hægt að afturkalla",
"pad.impexp.importbutton": "Flytja inn núna",
"pad.impexp.importing": "Flyt inn...",
"pad.impexp.confirmimport": "Innflutningur á skrá mun skrifa yfir þann texta sem er á skrifblokkinni núna. \nErtu viss um að þú viljir halda áfram?",
diff --git a/src/locales/pt.json b/src/locales/pt.json
index 3e0345ce2..b770bd15d 100644
--- a/src/locales/pt.json
+++ b/src/locales/pt.json
@@ -9,6 +9,7 @@
"Luckas",
"Macofe",
"Mansil alfalb",
+ "MuratTheTurkish",
"Ti4goc",
"Tuliouel",
"Waldir",
@@ -110,7 +111,7 @@
"timeslider.toolbar.exportlink.title": "Exportar",
"timeslider.exportCurrent": "Exportar versão atual como:",
"timeslider.version": "Versão {{version}}",
- "timeslider.saved": "Gravado a {{day}} de {{month}} de {{ano}}",
+ "timeslider.saved": "Gravado a {{day}} de {{month}} de {{year}}",
"timeslider.playPause": "Reproduzir / pausar conteúdo da nota",
"timeslider.backRevision": "Voltar a uma revisão anterior desta nota",
"timeslider.forwardRevision": "Avançar para uma revisão posterior desta nota",
diff --git a/src/locales/sv.json b/src/locales/sv.json
index 07f4460b4..fcef0a10c 100644
--- a/src/locales/sv.json
+++ b/src/locales/sv.json
@@ -1,6 +1,7 @@
{
"@metadata": {
"authors": [
+ "Bengtsson96",
"Jopparn",
"Lokal Profil",
"Sabelöga",
@@ -15,7 +16,7 @@
"pad.toolbar.underline.title": "Understruken (Ctrl+U)",
"pad.toolbar.strikethrough.title": "Genomstruken (Ctrl+5)",
"pad.toolbar.ol.title": "Numrerad lista (Ctrl+Shift+N)",
- "pad.toolbar.ul.title": "Onumrerad lista (Ctrl+Shift+L)",
+ "pad.toolbar.ul.title": "Punktlista (Ctrl+Shift+L)",
"pad.toolbar.indent.title": "Öka indrag (TABB)",
"pad.toolbar.unindent.title": "Minska indrag (Shift+TABB)",
"pad.toolbar.undo.title": "Ångra (Ctrl+Z)",
@@ -29,7 +30,7 @@
"pad.toolbar.showusers.title": "Visa användarna på detta block",
"pad.colorpicker.save": "Spara",
"pad.colorpicker.cancel": "Avbryt",
- "pad.loading": "Läser in...",
+ "pad.loading": "Läser in …",
"pad.noCookie": "Kunde inte hitta några kakor. Var god tillåt kakor i din webbläsare! Din session och inställningar kommer inte sparas mellan dina besök. Detta kan bero på att Etherpad inte ligger inuti en iFrame i vissa webbläsare. Se till att Etherpad är i samma underdomän/domän som det överordnade iFrame-elementet.",
"pad.passwordRequired": "Du behöver ett lösenord för att få tillgång till detta block",
"pad.permissionDenied": "Du har inte åtkomstbehörighet för detta block",
@@ -58,7 +59,7 @@
"pad.importExport.exportopen": "ODF (Open Document Format)",
"pad.importExport.abiword.innerHTML": "Du kan endast importera från oformaterad text eller HTML-format. För mer avancerade importfunktioner, var god installera AbiWord eller LibreOffice.",
"pad.modals.connected": "Ansluten.",
- "pad.modals.reconnecting": "Återansluter till ditt block...",
+ "pad.modals.reconnecting": "Återansluter till ditt block …",
"pad.modals.forcereconnect": "Tvinga återanslutning",
"pad.modals.reconnecttimer": "Försöker ansluta igen",
"pad.modals.cancel": "Avbryt",
@@ -129,7 +130,7 @@
"pad.userlist.approve": "Godkänn",
"pad.editbar.clearcolors": "Rensa författarfärger för hela dokumentet? Detta kan inte ångras",
"pad.impexp.importbutton": "Importera nu",
- "pad.impexp.importing": "Importerar...",
+ "pad.impexp.importing": "Importerar …",
"pad.impexp.confirmimport": "Att importera en fil kommer att skriva över den aktuella texten i blocket. Är du säker på att du vill fortsätta?",
"pad.impexp.convertFailed": "Vi kunde inte importera denna fil. Var god använd ett annat dokumentformat eller kopiera och klistra in den manuellt",
"pad.impexp.padHasData": "Vi kunde inte importera denna fil eftersom detta block redan har redigerats. Importera den till ett nytt block.",
diff --git a/src/node/db/SessionManager.js b/src/node/db/SessionManager.js
index 5f7df1e24..5ba43c462 100644
--- a/src/node/db/SessionManager.js
+++ b/src/node/db/SessionManager.js
@@ -72,7 +72,7 @@ exports.findAuthorID = async (groupID, sessionCookie) => {
return undefined;
});
const now = Math.floor(Date.now() / 1000);
- const isMatch = (si) => (si != null && si.groupID === groupID && si.validUntil <= now);
+ const isMatch = (si) => (si != null && si.groupID === groupID && now < si.validUntil);
const sessionInfo = await promises.firstSatisfies(sessionInfoPromises, isMatch);
if (sessionInfo == null) return undefined;
return sessionInfo.authorID;
diff --git a/src/node/db/SessionStore.js b/src/node/db/SessionStore.js
index 647cbbc8d..e265ee68e 100644
--- a/src/node/db/SessionStore.js
+++ b/src/node/db/SessionStore.js
@@ -7,92 +7,37 @@
* express-session, which can't actually use promises anyway.
*/
-var Store = require('ep_etherpad-lite/node_modules/express-session').Store,
- db = require('ep_etherpad-lite/node/db/DB').db,
- log4js = require('ep_etherpad-lite/node_modules/log4js'),
- messageLogger = log4js.getLogger("SessionStore");
+const DB = require('ep_etherpad-lite/node/db/DB');
+const Store = require('ep_etherpad-lite/node_modules/express-session').Store;
+const log4js = require('ep_etherpad-lite/node_modules/log4js');
-var SessionStore = module.exports = function SessionStore() {};
+const logger = log4js.getLogger('SessionStore');
-SessionStore.prototype.__proto__ = Store.prototype;
-
-SessionStore.prototype.get = function(sid, fn) {
- messageLogger.debug('GET ' + sid);
-
- var self = this;
-
- db.get("sessionstorage:" + sid, function(err, sess) {
- if (sess) {
- sess.cookie.expires = 'string' == typeof sess.cookie.expires ? new Date(sess.cookie.expires) : sess.cookie.expires;
- if (!sess.cookie.expires || new Date() < sess.cookie.expires) {
- fn(null, sess);
+module.exports = class SessionStore extends Store {
+ get(sid, fn) {
+ logger.debug('GET ' + sid);
+ DB.db.get('sessionstorage:' + sid, (err, sess) => {
+ if (sess) {
+ sess.cookie.expires = ('string' == typeof sess.cookie.expires
+ ? new Date(sess.cookie.expires) : sess.cookie.expires);
+ if (!sess.cookie.expires || new Date() < sess.cookie.expires) {
+ fn(null, sess);
+ } else {
+ this.destroy(sid, fn);
+ }
} else {
- self.destroy(sid, fn);
- }
- } else {
- fn();
- }
- });
-};
-
-SessionStore.prototype.set = function(sid, sess, fn) {
- messageLogger.debug('SET ' + sid);
-
- db.set("sessionstorage:" + sid, sess);
- if (fn) {
- process.nextTick(fn);
- }
-};
-
-SessionStore.prototype.destroy = function(sid, fn) {
- messageLogger.debug('DESTROY ' + sid);
-
- db.remove("sessionstorage:" + sid);
- if (fn) {
- process.nextTick(fn);
- }
-};
-
-/*
- * RPB: the following methods are optional requirements for a compatible session
- * store for express-session, but in any case appear to depend on a
- * non-existent feature of ueberdb2
- */
-if (db.forEach) {
- SessionStore.prototype.all = function(fn) {
- messageLogger.debug('ALL');
-
- var sessions = [];
-
- db.forEach(function(key, value) {
- if (key.substr(0,15) === "sessionstorage:") {
- sessions.push(value);
+ fn();
}
});
- fn(null, sessions);
- };
+ }
- SessionStore.prototype.clear = function(fn) {
- messageLogger.debug('CLEAR');
+ set(sid, sess, fn) {
+ logger.debug('SET ' + sid);
+ DB.db.set('sessionstorage:' + sid, sess, fn);
+ }
- db.forEach(function(key, value) {
- if (key.substr(0,15) === "sessionstorage:") {
- db.remove("session:" + key);
- }
- });
- if (fn) fn();
- };
-
- SessionStore.prototype.length = function(fn) {
- messageLogger.debug('LENGTH');
-
- var i = 0;
-
- db.forEach(function(key, value) {
- if (key.substr(0,15) === "sessionstorage:") {
- i++;
- }
- });
- fn(null, i);
+ destroy(sid, fn) {
+ logger.debug('DESTROY ' + sid);
+ DB.db.remove('sessionstorage:' + sid, fn);
}
};
diff --git a/src/node/utils/promises.js b/src/node/utils/promises.js
index a754823a0..bb973befa 100644
--- a/src/node/utils/promises.js
+++ b/src/node/utils/promises.js
@@ -35,27 +35,21 @@ exports.firstSatisfies = (promises, predicate) => {
return Promise.race(newPromises);
};
-exports.timesLimit = function(ltMax, concurrency, promiseCreator) {
- var done = 0
- var current = 0
-
- function addAnother () {
- function _internalRun () {
- done++
-
- if (done < ltMax) {
- addAnother()
- }
- }
-
- promiseCreator(current)
- .then(_internalRun)
- .catch(_internalRun)
-
- current++
- }
-
- for (var i = 0; i < concurrency && i < ltMax; i++) {
- addAnother()
+// Calls `promiseCreator(i)` a total number of `total` times, where `i` is 0 through `total - 1` (in
+// order). The `concurrency` argument specifies the maximum number of Promises returned by
+// `promiseCreator` that are allowed to be active (unresolved) simultaneously. (In other words: If
+// `total` is greater than `concurrency`, then `concurrency` Promises will be created right away,
+// and each remaining Promise will be created once one of the earlier Promises resolves.) This async
+// function resolves once all `total` Promises have resolved.
+exports.timesLimit = async (total, concurrency, promiseCreator) => {
+ if (total > 0 && concurrency <= 0) throw new RangeError('concurrency must be positive');
+ let next = 0;
+ const addAnother = () => promiseCreator(next++).finally(() => {
+ if (next < total) return addAnother();
+ });
+ const promises = [];
+ for (var i = 0; i < concurrency && i < total; i++) {
+ promises.push(addAnother());
}
+ await Promise.all(promises);
}
diff --git a/src/package.json b/src/package.json
index dfb350583..b01e07a02 100644
--- a/src/package.json
+++ b/src/package.json
@@ -92,10 +92,9 @@
"url": "https://github.com/ether/etherpad-lite.git"
},
"scripts": {
- "test": "nyc wtfnode node_modules/.bin/_mocha --timeout 5000 --recursive ../tests/backend/specs",
- "test-contentcollector": "nyc mocha --timeout 5000 ../tests/backend/specs",
+ "test": "nyc wtfnode node_modules/.bin/_mocha --timeout 5000 --recursive ../tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
"test-container": "nyc mocha --timeout 5000 ../tests/container/specs/api"
},
- "version": "1.8.5",
+ "version": "1.8.6",
"license": "Apache-2.0"
}
diff --git a/src/static/css/pad/layout.css b/src/static/css/pad/layout.css
index 7b64936d7..e5b79c268 100644
--- a/src/static/css/pad/layout.css
+++ b/src/static/css/pad/layout.css
@@ -47,8 +47,6 @@ body {
width: 0; /* hide when the container is empty */
}
-@media only screen and (max-width: 800px) {
- #editorcontainerbox {
- margin-bottom: 39px; /* Leave space for the bottom toolbar on mobile */
- }
+.mobile-layout #editorcontainerbox {
+ margin-bottom: 39px; /* Leave space for the bottom toolbar on mobile */
}
diff --git a/src/static/css/pad/popup.css b/src/static/css/pad/popup.css
index 00fc8ca51..0eb000996 100644
--- a/src/static/css/pad/popup.css
+++ b/src/static/css/pad/popup.css
@@ -78,9 +78,11 @@
.popup#users .popup-content {
overflow: visible;
}
- /* Move popup to the bottom, except popup linked to left toolbar, like hyperklink popup */
- .popup:not(.toolbar-popup) {
- top: auto;
- bottom: 1rem;
- }
-}
\ No newline at end of file
+}
+/* Move popup to the bottom, except popup linked to left toolbar, like hyperklink popup */
+.mobile-layout .popup:not(.toolbar-popup) {
+ top: auto;
+ left: 1rem;
+ right: auto;
+ bottom: 1rem;
+}
diff --git a/src/static/css/pad/popup_users.css b/src/static/css/pad/popup_users.css
index 8b6ba82bc..ce4d14365 100644
--- a/src/static/css/pad/popup_users.css
+++ b/src/static/css/pad/popup_users.css
@@ -98,13 +98,15 @@ input#myusernameedit:not(.editable) {
right: calc(100% + 15px);
z-index: 101;
}
-@media (max-width: 800px) {
- #mycolorpicker.popup {
- top: auto;
- bottom: 0;
- left: auto !important;
- right: 0 !important;
- }
+.mobile-layout #users.popup {
+ right: 1rem;
+ left: auto;
+}
+.mobile-layout #mycolorpicker.popup {
+ top: auto;
+ bottom: 0;
+ left: auto !important;
+ right: 0 !important;
}
#mycolorpicker.popup .btn-container {
margin-top: 10px;
diff --git a/src/static/css/pad/toolbar.css b/src/static/css/pad/toolbar.css
index bc258510a..1a398b199 100644
--- a/src/static/css/pad/toolbar.css
+++ b/src/static/css/pad/toolbar.css
@@ -139,37 +139,40 @@
.toolbar ul li.separator {
width: 5px;
}
- /* menu_right act like a new toolbar on the bottom of the screen */
- .toolbar .menu_right {
- position: fixed;
- bottom: 0;
- right: 0;
- left: 0;
- border-top: 1px solid #ccc;
- background-color: #f4f4f4;
- padding: 0 5px 5px 5px;
- }
- .toolbar ul.menu_right > li {
- margin-right: 8px;
- }
- .toolbar ul.menu_right > li.separator {
- display: none;
- }
- .toolbar ul.menu_right > li a {
- border: none;
- background-color: transparent;
- margin-left: 5px;
- }
- .toolbar ul.menu_right > li[data-key="showusers"] {
- position: absolute;
- right: 0;
- top: 0;
- bottom: 0;
- margin: 0;
- }
- .toolbar ul.menu_right > li[data-key="showusers"] a {
- height: 100%;
- width: 40px;
- border-radius: 0;
- }
-}
\ No newline at end of file
+}
+
+/* menu_right act like a new toolbar on the bottom of the screen */
+.mobile-layout .toolbar .menu_right {
+ position: fixed;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ border-top: 1px solid #ccc;
+ background-color: #f4f4f4;
+ padding: 0 5px 5px 5px;
+}
+.mobile-layout .toolbar ul.menu_right > li {
+ margin-right: 8px;
+}
+.mobile-layout .toolbar ul.menu_right > li[data-key="showusers"] {
+ position: absolute;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ margin: 0;
+}
+.mobile-layout .toolbar ul.menu_right > li[data-key="showusers"] a {
+ height: 100%;
+ width: 40px;
+ border-radius: 0;
+}
+.mobile-layout .toolbar ul.menu_right > li.separator {
+ display: none;
+}
+.mobile-layout .toolbar ul.menu_right > li a {
+ border: none;
+ margin-left: 5px;
+}
+.mobile-layout .toolbar ul.menu_right > li a:not(.selected) {
+ background-color: transparent;
+}
diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.js
index 30d223059..0e40bb990 100644
--- a/src/static/js/pad_editbar.js
+++ b/src/static/js/pad_editbar.js
@@ -317,12 +317,14 @@ var padeditbar = (function()
{
// reset style
$('.toolbar').removeClass('cropped')
+ $('body').removeClass('mobile-layout');
var menu_left = $('.toolbar .menu_left')[0];
- // on mobile the menu_right get displayed at the bottom of the screen
- var isMobileLayout = $('.toolbar .menu_right').css('position') === 'fixed';
-
- if (menu_left && menu_left.scrollWidth > $('.toolbar').width() && isMobileLayout) {
+ var menuRightWidth = 280; // this is approximate, we cannot measure it because on mobileLayour it takes the full width on the bottom of the page
+ if (menu_left && menu_left.scrollWidth > $('.toolbar').width() - menuRightWidth || $('.toolbar').width() < 1000) {
+ $('body').addClass('mobile-layout');
+ }
+ if (menu_left && menu_left.scrollWidth > $('.toolbar').width()) {
$('.toolbar').addClass('cropped');
}
}
diff --git a/src/static/skins/colibris/src/components/toolbar.css b/src/static/skins/colibris/src/components/toolbar.css
index 91e9991ed..7f3e71403 100644
--- a/src/static/skins/colibris/src/components/toolbar.css
+++ b/src/static/skins/colibris/src/components/toolbar.css
@@ -131,23 +131,24 @@
}
}
-@media (max-width: 800px) {
-
- .toolbar ul li {
- margin: 5px 2px;
- }
-
- .toolbar .menu_right {
- border-top: 1px solid #d2d2d2;
- border-top: var(--toolbar-border);
- background-color: #ffffff;
- background-color: var(--bg-color);
- padding: 0;
- }
-
- .toolbar ul li a:hover { background-color: transparent; }
-
- .toolbar ul li.separator { margin: 0; display: none; }
+.mobile-layout .toolbar ul li {
+ margin: 5px 2px;
+}
+.mobile-layout .toolbar ul li.separator {
+ margin: 0 5px;
+}
+@media (max-width: 800px) {
+ .mobile-layout .toolbar ul li.separator {
+ display: none;
+ }
+}
+.mobile-layout .toolbar .menu_right {
+ border-top: 1px solid #d2d2d2;
+ border-top: var(--toolbar-border);
+ background-color: #ffffff;
+ background-color: var(--bg-color);
+ padding: 0;
+}
+.mobile-layout .toolbar ul li a:hover {
+ /* background-color: transparent; */
}
-
-
diff --git a/src/static/skins/colibris/src/layout.css b/src/static/skins/colibris/src/layout.css
index 6385cf140..1ec3886c8 100644
--- a/src/static/skins/colibris/src/layout.css
+++ b/src/static/skins/colibris/src/layout.css
@@ -46,10 +46,3 @@
border-radius: 0;
}
}
-
-@media only screen and (max-width: 800px) {
- #editorcontainerbox {
- margin-bottom: 39px; /* margin for bottom toolbar */
- }
-}
-
diff --git a/tests/backend/specs/promises.js b/tests/backend/specs/promises.js
new file mode 100644
index 000000000..13a8c532a
--- /dev/null
+++ b/tests/backend/specs/promises.js
@@ -0,0 +1,85 @@
+function m(mod) { return __dirname + '/../../../src/' + mod; }
+
+const assert = require('assert').strict;
+const promises = require(m('node/utils/promises'));
+
+describe('promises.timesLimit', async () => {
+ let wantIndex = 0;
+ const testPromises = [];
+ const makePromise = (index) => {
+ // Make sure index increases by one each time.
+ assert.equal(index, wantIndex++);
+ // Save the resolve callback (so the test can trigger resolution)
+ // and the promise itself (to wait for resolve to take effect).
+ const p = {};
+ const promise = new Promise((resolve) => {
+ p.resolve = resolve;
+ });
+ p.promise = promise;
+ testPromises.push(p);
+ return p.promise;
+ };
+
+ const total = 11;
+ const concurrency = 7;
+ const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise);
+
+ it('honors concurrency', async () => {
+ assert.equal(wantIndex, concurrency);
+ });
+
+ it('creates another when one completes', async () => {
+ const {promise, resolve} = testPromises.shift();
+ resolve();
+ await promise;
+ assert.equal(wantIndex, concurrency + 1);
+ });
+
+ it('creates the expected total number of promises', async () => {
+ while (testPromises.length > 0) {
+ // Resolve them in random order to ensure that the resolution order doesn't matter.
+ const i = Math.floor(Math.random() * Math.floor(testPromises.length));
+ const {promise, resolve} = testPromises.splice(i, 1)[0];
+ resolve();
+ await promise;
+ }
+ assert.equal(wantIndex, total);
+ });
+
+ it('resolves', async () => {
+ await timesLimitPromise;
+ });
+
+ it('does not create too many promises if total < concurrency', async () => {
+ wantIndex = 0;
+ assert.equal(testPromises.length, 0);
+ const total = 7;
+ const concurrency = 11;
+ const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise);
+ while (testPromises.length > 0) {
+ const {promise, resolve} = testPromises.pop();
+ resolve();
+ await promise;
+ }
+ await timesLimitPromise;
+ assert.equal(wantIndex, total);
+ });
+
+ it('accepts total === 0, concurrency > 0', async () => {
+ wantIndex = 0;
+ assert.equal(testPromises.length, 0);
+ await promises.timesLimit(0, concurrency, makePromise);
+ assert.equal(wantIndex, 0);
+ });
+
+ it('accepts total === 0, concurrency === 0', async () => {
+ wantIndex = 0;
+ assert.equal(testPromises.length, 0);
+ await promises.timesLimit(0, 0, makePromise);
+ assert.equal(wantIndex, 0);
+ });
+
+ it('rejects total > 0, concurrency === 0', async () => {
+ await assert.rejects(promises.timesLimit(total, 0, makePromise), RangeError);
+ });
+});
diff --git a/tests/frontend/travis/runnerBackend.sh b/tests/frontend/travis/runnerBackend.sh
index 680e96f9e..c595dce02 100755
--- a/tests/frontend/travis/runnerBackend.sh
+++ b/tests/frontend/travis/runnerBackend.sh
@@ -46,6 +46,5 @@ cd src
failed=0
npm run test || failed=1
-npm run test-contentcollector || failed=1
exit $failed