2020-11-25 19:35:44 +01:00
|
|
|
'use strict';
|
|
|
|
|
2021-05-10 00:45:43 +02:00
|
|
|
// $(handler), $().ready(handler), $.wait($.ready).then(handler), etc. don't work if handler is an
|
|
|
|
// async function for some bizarre reason, so the async function is wrapped in a non-async function.
|
|
|
|
$(() => (async () => {
|
2020-11-25 19:35:44 +01:00
|
|
|
const stringifyException = (exception) => {
|
2020-11-23 19:21:51 +01:00
|
|
|
let err = exception.stack || exception.toString();
|
2020-06-07 10:53:10 +02:00
|
|
|
|
|
|
|
// FF / Opera do not add the message
|
|
|
|
if (!~err.indexOf(exception.message)) {
|
2020-11-23 19:21:51 +01:00
|
|
|
err = `${exception.message}\n${err}`;
|
2020-06-07 10:53:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// <=IE7 stringifies to [Object Error]. Since it can be overloaded, we
|
|
|
|
// check for the result of the stringifying.
|
2020-11-25 19:35:44 +01:00
|
|
|
if (err === '[object Error]') err = exception.message;
|
2020-06-07 10:53:10 +02:00
|
|
|
|
|
|
|
// Safari doesn't give you a stack. Let's at least provide a source line.
|
|
|
|
if (!exception.stack && exception.sourceURL && exception.line !== undefined) {
|
2020-11-23 19:21:51 +01:00
|
|
|
err += `\n(${exception.sourceURL}:${exception.line})`;
|
2020-06-07 10:53:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return err;
|
2020-11-25 19:35:44 +01:00
|
|
|
};
|
2020-06-07 10:53:10 +02:00
|
|
|
|
2020-11-25 19:35:44 +01:00
|
|
|
const customRunner = (runner) => {
|
2020-11-23 19:21:51 +01:00
|
|
|
const stats = {suites: 0, tests: 0, passes: 0, pending: 0, failures: 0};
|
2020-11-25 19:35:44 +01:00
|
|
|
let level = 0;
|
2012-11-03 15:31:33 +01:00
|
|
|
|
|
|
|
if (!runner) return;
|
|
|
|
|
2021-06-09 06:33:04 +02:00
|
|
|
// AUTO-SCROLLING:
|
|
|
|
|
|
|
|
// Mocha can start multiple suites before the first 'suite' event is emitted. This can break the
|
|
|
|
// logic used to determine if the div is already scrolled to the bottom. If this is false,
|
|
|
|
// auto-scrolling unconditionally scrolls to the bottom no matter how far up the div is
|
|
|
|
// currently scrolled. If true, auto-scrolling only happens if the div is scrolled close to the
|
|
|
|
// bottom.
|
|
|
|
let manuallyScrolled = false;
|
|
|
|
// The 'scroll' event is fired for manual scrolling as well as JavaScript-initiated scrolling.
|
|
|
|
// This is incremented while auto-scrolling and decremented when done auto-scrolling. This is
|
|
|
|
// used to ensure that auto-scrolling never sets manuallyScrolled to true.
|
|
|
|
let autoScrolling = 0;
|
|
|
|
|
|
|
|
// Auto-scroll the #mocha-report div to show the newly added test entry if it was previously
|
|
|
|
// scrolled to the bottom.
|
|
|
|
const autoscroll = (newElement) => {
|
|
|
|
const mr = $('#mocha-report')[0];
|
|
|
|
const scroll = !manuallyScrolled || (() => {
|
|
|
|
const offsetTopAbs = newElement.getBoundingClientRect().top;
|
|
|
|
const mrOffsetTopAbs = mr.getBoundingClientRect().top - mr.scrollTop;
|
|
|
|
const offsetTop = offsetTopAbs - mrOffsetTopAbs;
|
|
|
|
// Add some margin to cover rounding error and to make it easier to engage the auto-scroll.
|
|
|
|
return offsetTop <= mr.clientHeight + mr.scrollTop + 5;
|
|
|
|
})();
|
|
|
|
if (!scroll) return;
|
|
|
|
++autoScrolling;
|
|
|
|
mr.scrollTop = mr.scrollHeight;
|
|
|
|
manuallyScrolled = false;
|
|
|
|
};
|
|
|
|
|
|
|
|
$('#mocha-report').on('scroll', () => {
|
|
|
|
if (!autoScrolling) manuallyScrolled = true;
|
|
|
|
else --autoScrolling;
|
|
|
|
});
|
|
|
|
|
2020-11-23 19:21:51 +01:00
|
|
|
runner.on('start', () => {
|
|
|
|
stats.start = new Date();
|
2012-11-03 15:31:33 +01:00
|
|
|
});
|
|
|
|
|
2020-11-23 19:21:51 +01:00
|
|
|
runner.on('suite', (suite) => {
|
2020-06-07 10:53:10 +02:00
|
|
|
if (suite.root) return;
|
2021-06-09 06:33:04 +02:00
|
|
|
autoscroll($('#mocha-report .suite').last()[0]);
|
|
|
|
stats.suites++;
|
2020-06-07 10:53:10 +02:00
|
|
|
append(suite.title);
|
|
|
|
level++;
|
|
|
|
});
|
|
|
|
|
2020-11-23 19:21:51 +01:00
|
|
|
runner.on('suite end', (suite) => {
|
2020-06-07 10:53:10 +02:00
|
|
|
if (suite.root) return;
|
|
|
|
level--;
|
|
|
|
|
2020-11-25 19:35:44 +01:00
|
|
|
if (level === 0) {
|
2020-11-23 19:21:51 +01:00
|
|
|
append('');
|
2020-06-07 10:53:10 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-07-28 20:57:33 +02:00
|
|
|
// max time a test is allowed to run
|
|
|
|
// TODO this should be lowered once timeslider_revision.js is faster
|
2020-11-23 19:21:51 +01:00
|
|
|
let killTimeout;
|
|
|
|
runner.on('test end', () => {
|
2021-06-09 06:33:04 +02:00
|
|
|
autoscroll($('#mocha-report .test').last()[0]);
|
2012-11-03 15:31:33 +01:00
|
|
|
stats.tests++;
|
2020-07-28 20:57:33 +02:00
|
|
|
});
|
2020-06-07 10:53:10 +02:00
|
|
|
|
2020-11-23 19:21:51 +01:00
|
|
|
runner.on('pass', (test) => {
|
|
|
|
if (killTimeout) clearTimeout(killTimeout);
|
|
|
|
killTimeout = setTimeout(() => {
|
2021-02-12 21:04:42 +01:00
|
|
|
append('FINISHED - [red]no test started since 5 minutes, tests stopped[clear]');
|
|
|
|
}, 60000 * 5);
|
2012-11-03 15:31:33 +01:00
|
|
|
|
2020-11-23 19:21:51 +01:00
|
|
|
const medium = test.slow() / 2;
|
2012-11-03 15:31:33 +01:00
|
|
|
test.speed = test.duration > test.slow()
|
|
|
|
? 'slow'
|
|
|
|
: test.duration > medium
|
|
|
|
? 'medium'
|
|
|
|
: 'fast';
|
|
|
|
|
|
|
|
stats.passes++;
|
2020-11-25 19:52:47 +01:00
|
|
|
append(`-> [green]PASSED[clear] : ${test.title} ${test.duration} ms`);
|
2012-11-03 15:31:33 +01:00
|
|
|
});
|
|
|
|
|
2020-11-23 19:21:51 +01:00
|
|
|
runner.on('fail', (test, err) => {
|
|
|
|
if (killTimeout) clearTimeout(killTimeout);
|
|
|
|
killTimeout = setTimeout(() => {
|
2021-02-12 21:04:42 +01:00
|
|
|
append('FINISHED - [red]no test started since 5 minutes, tests stopped[clear]');
|
|
|
|
}, 60000 * 5);
|
2020-07-28 20:57:33 +02:00
|
|
|
|
2012-11-03 15:31:33 +01:00
|
|
|
stats.failures++;
|
|
|
|
test.err = err;
|
2020-11-25 19:52:47 +01:00
|
|
|
append(`-> [red]FAILED[clear] : ${test.title} ${stringifyException(test.err)}`);
|
2012-11-03 15:31:33 +01:00
|
|
|
});
|
|
|
|
|
2020-11-23 19:21:51 +01:00
|
|
|
runner.on('pending', (test) => {
|
|
|
|
if (killTimeout) clearTimeout(killTimeout);
|
|
|
|
killTimeout = setTimeout(() => {
|
2021-02-12 21:04:42 +01:00
|
|
|
append('FINISHED - [red]no test started since 5 minutes, tests stopped[clear]');
|
|
|
|
}, 60000 * 5);
|
2012-11-03 15:31:33 +01:00
|
|
|
|
|
|
|
stats.pending++;
|
2020-11-25 19:52:47 +01:00
|
|
|
append(`-> [yellow]PENDING[clear]: ${test.title}`);
|
2012-11-03 15:31:33 +01:00
|
|
|
});
|
2012-11-18 12:49:34 +01:00
|
|
|
|
2020-11-23 19:21:51 +01:00
|
|
|
const $console = $('#console');
|
2020-11-25 19:52:47 +01:00
|
|
|
const append = (text) => {
|
2021-05-03 02:31:25 +02:00
|
|
|
// Indent each line.
|
|
|
|
const lines = text.split('\n').map((line) => ' '.repeat(level * 2) + line);
|
2021-05-03 02:46:00 +02:00
|
|
|
$console.append(document.createTextNode(`${lines.join('\n')}\n`));
|
2020-11-23 19:21:51 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
const total = runner.total;
|
|
|
|
runner.on('end', () => {
|
|
|
|
stats.end = new Date();
|
|
|
|
stats.duration = stats.end - stats.start;
|
|
|
|
const minutes = Math.floor(stats.duration / 1000 / 60);
|
2020-11-25 19:35:44 +01:00
|
|
|
// chrome < 57 does not like this .toString().padStart('2', '0');
|
|
|
|
const seconds = Math.round((stats.duration / 1000) % 60);
|
2020-11-23 19:21:51 +01:00
|
|
|
if (stats.tests === total) {
|
2020-11-25 19:52:47 +01:00
|
|
|
append(`FINISHED - ${stats.passes} tests passed, ${stats.failures} tests failed, ` +
|
|
|
|
`${stats.pending} pending, duration: ${minutes}:${seconds}`);
|
2020-11-23 19:21:51 +01:00
|
|
|
} else if (stats.tests > total) {
|
2020-11-25 19:52:47 +01:00
|
|
|
append(`FINISHED - but more tests than planned returned ${stats.passes} tests passed, ` +
|
|
|
|
`${stats.failures} tests failed, ${stats.pending} pending, ` +
|
|
|
|
`duration: ${minutes}:${seconds}`);
|
|
|
|
append(`${total} tests, but ${stats.tests} returned. ` +
|
|
|
|
'There is probably a problem with your async code or error handling, ' +
|
|
|
|
'see https://github.com/mochajs/mocha/issues/1327');
|
2020-11-23 19:21:51 +01:00
|
|
|
} else {
|
2020-11-25 19:52:47 +01:00
|
|
|
append(`FINISHED - but not all tests returned ${stats.passes} tests passed, ` +
|
|
|
|
`${stats.failures} tests failed, ${stats.pending} tests pending, ` +
|
|
|
|
`duration: ${minutes}:${seconds}`);
|
|
|
|
append(`${total} tests, but only ${stats.tests} returned. ` +
|
|
|
|
'Check for failed before/beforeEach-hooks (no `test end` is called for them ' +
|
|
|
|
'and subsequent tests of the same suite are skipped), ' +
|
|
|
|
'see https://github.com/mochajs/mocha/pull/1043');
|
2020-11-23 19:21:51 +01:00
|
|
|
}
|
|
|
|
});
|
2020-11-25 19:35:44 +01:00
|
|
|
};
|
2012-10-28 14:16:41 +01:00
|
|
|
|
2020-11-25 19:18:11 +01:00
|
|
|
const getURLParameter = (name) => (new URLSearchParams(location.search)).get(name);
|
2020-11-23 19:21:51 +01:00
|
|
|
|
2021-04-12 07:35:27 +02:00
|
|
|
const absUrl = (url) => new URL(url, window.location.href).href;
|
|
|
|
require.setRootURI(absUrl('../../javascripts/src'));
|
|
|
|
require.setLibraryURI(absUrl('../../javascripts/lib'));
|
|
|
|
require.setGlobalKeyPath('require');
|
|
|
|
|
2021-06-05 09:34:55 +02:00
|
|
|
const Split = require('split-grid/dist/split-grid.min');
|
|
|
|
new Split({
|
|
|
|
columnGutters: [{
|
|
|
|
track: 1,
|
|
|
|
element: document.getElementById('separator'),
|
|
|
|
}],
|
|
|
|
});
|
|
|
|
|
2021-06-10 01:54:36 +02:00
|
|
|
// Speed up tests by loading test definitions in parallel. Approach: Define a new global object
|
|
|
|
// that has a define() method, which is a wrapper around window.require.define(). The wrapper
|
|
|
|
// mutates the module definition function to temporarily replace Mocha's functions with
|
|
|
|
// placeholders. The placeholders make it possible to defer the actual Mocha function calls until
|
|
|
|
// after the modules are all loaded in parallel. require.setGlobalKeyPath() is used to coax
|
|
|
|
// require-kernel into using the wrapper define() method instead of require.define().
|
|
|
|
|
|
|
|
// Per-module log of attempted Mocha function calls. Key is module path, value is an array of
|
|
|
|
// [functionName, argsArray] arrays.
|
|
|
|
const mochaCalls = new Map();
|
2021-06-14 10:22:32 +02:00
|
|
|
const mochaFns = [
|
|
|
|
'after',
|
|
|
|
'afterEach',
|
|
|
|
'before',
|
|
|
|
'beforeEach',
|
|
|
|
'context',
|
|
|
|
'describe',
|
|
|
|
'it',
|
|
|
|
'run',
|
|
|
|
'specify',
|
|
|
|
'xcontext', // Undocumented as of Mocha 7.1.2.
|
|
|
|
'xdescribe', // Undocumented as of Mocha 7.1.2.
|
|
|
|
'xit', // Undocumented as of Mocha 7.1.2.
|
|
|
|
'xspecify', // Undocumented as of Mocha 7.1.2.
|
|
|
|
];
|
2021-06-10 01:54:36 +02:00
|
|
|
window.testRunnerRequire = {
|
|
|
|
define(...args) {
|
|
|
|
if (args.length === 2) args = [{[args[0]]: args[1]}];
|
|
|
|
if (args.length !== 1) throw new Error('unexpected args passed to testRunnerRequire.define');
|
|
|
|
const [origDefs] = args;
|
|
|
|
const defs = {};
|
|
|
|
for (const [path, origDef] of Object.entries(origDefs)) {
|
2021-10-27 22:12:35 +02:00
|
|
|
defs[path] = origDef == null ? origDef : function (require, exports, module) {
|
2021-06-10 01:54:36 +02:00
|
|
|
const calls = [];
|
|
|
|
mochaCalls.set(module.id.replace(/\.js$/, ''), calls);
|
|
|
|
// Backup Mocha functions. Note that because modules can require other modules, these
|
|
|
|
// backups might be placeholders, not the actual Mocha functions.
|
|
|
|
const backups = {};
|
|
|
|
for (const fn of mochaFns) {
|
2021-06-14 10:22:46 +02:00
|
|
|
if (typeof window[fn] !== 'function') continue;
|
2021-06-10 01:54:36 +02:00
|
|
|
// Note: Test specs can require other modules, so window[fn] might be a placeholder
|
|
|
|
// function, not the actual Mocha function.
|
|
|
|
backups[fn] = window[fn];
|
|
|
|
window[fn] = (...args) => calls.push([fn, args]);
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
return origDef.call(this, require, exports, module);
|
|
|
|
} finally {
|
|
|
|
Object.assign(window, backups);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return require.define(defs);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
require.setGlobalKeyPath('testRunnerRequire');
|
|
|
|
// Increase fetch parallelism to speed up test spec loading. (Note: The browser might limit to a
|
|
|
|
// lower value -- this is just an upper limit.)
|
|
|
|
require.setRequestMaximum(20);
|
2021-06-09 06:34:33 +02:00
|
|
|
|
|
|
|
const $log = $('<div>');
|
|
|
|
const appendToLog = (msg) => {
|
|
|
|
if (typeof msg === 'string') msg = document.createTextNode(msg);
|
|
|
|
// Add some margin to cover rounding error and to make it easier to engage the auto-scroll.
|
|
|
|
const scrolledToBottom = $log[0].scrollHeight <= $log[0].scrollTop + $log[0].clientHeight + 5;
|
|
|
|
const $msg = $('<div>').css('white-space', 'nowrap').append(msg).appendTo($log);
|
|
|
|
if (scrolledToBottom) $log[0].scrollTop = $log[0].scrollHeight;
|
|
|
|
return $msg;
|
|
|
|
};
|
|
|
|
const $bar = $('<progress>');
|
|
|
|
let barLastUpdate = Date.now();
|
|
|
|
const incrementBar = async (amount = 1) => {
|
|
|
|
$bar.attr('value', Number.parseInt($bar.attr('value')) + 1);
|
|
|
|
// Give the browser an opportunity to draw the progress bar's new length. `await
|
|
|
|
// Promise.resolve()` isn't enough, so a timeout is used. Sleeping every increment (even 0ms)
|
|
|
|
// unnecessarily slows down spec loading so the sleep is occasional.
|
|
|
|
if (Date.now() - barLastUpdate > 100) {
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
|
|
barLastUpdate = Date.now();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
const $progressArea = $('<div>')
|
|
|
|
.css({'display': 'flex', 'flex-direction': 'column', 'height': '100%'})
|
|
|
|
.append($('<div>').css({flex: '1 0 0'}))
|
|
|
|
.append($('<div>')
|
2021-06-10 19:28:55 +02:00
|
|
|
.css({'flex': '0 0 auto', 'font-weight': 'bold'})
|
2021-06-09 06:34:33 +02:00
|
|
|
.text('Loading frontend test specs...'))
|
|
|
|
.append($log.css({flex: '0 1 auto', overflow: 'auto'}))
|
|
|
|
.append($bar.css({flex: '0 0 auto', width: '100%'}))
|
|
|
|
.appendTo('#mocha');
|
|
|
|
const specs = await $.getJSON('frontendTestSpecs.json');
|
|
|
|
if (specs.length > 0) {
|
2021-06-10 01:54:36 +02:00
|
|
|
$bar.attr({value: 0, max: specs.length * 2});
|
2021-06-09 06:34:33 +02:00
|
|
|
await incrementBar(0);
|
|
|
|
}
|
|
|
|
const makeDesc = (spec) => `${spec
|
|
|
|
.replace(/^ep_etherpad-lite\/tests\/frontend\/specs\//, '<core> ')
|
|
|
|
.replace(/^([^/ ]*)\/static\/tests\/frontend\/specs\//, '<$1> ')}.js`;
|
2021-06-10 01:54:36 +02:00
|
|
|
await Promise.all(specs.map(async (spec) => {
|
|
|
|
const $msg = appendToLog(`Fetching ${makeDesc(spec)}...`);
|
|
|
|
try {
|
|
|
|
await new Promise((resolve, reject) => require(spec, (module) => {
|
|
|
|
if (module == null) return reject(new Error(`failed to load module ${spec}`));
|
|
|
|
resolve();
|
|
|
|
}));
|
|
|
|
} catch (err) {
|
|
|
|
$msg.append($('<b>').css('color', 'red').text(' FAILED'));
|
|
|
|
appendToLog($('<pre>').text(`${err.stack || err}`));
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
$msg.append(' done');
|
|
|
|
await incrementBar();
|
|
|
|
}));
|
|
|
|
require.setGlobalKeyPath('require');
|
|
|
|
delete window.testRunnerRequire;
|
2021-06-09 06:34:33 +02:00
|
|
|
for (const spec of specs) {
|
|
|
|
const desc = makeDesc(spec);
|
2021-06-10 01:54:36 +02:00
|
|
|
const $msg = appendToLog(`Executing ${desc}...`);
|
2021-06-09 06:34:33 +02:00
|
|
|
try {
|
2021-06-10 01:54:36 +02:00
|
|
|
describe(desc, function () {
|
|
|
|
for (const [fn, args] of mochaCalls.get(spec)) window[fn](...args);
|
|
|
|
});
|
2021-06-09 06:34:33 +02:00
|
|
|
} catch (err) {
|
|
|
|
$msg.append($('<b>').css('color', 'red').text(' FAILED'));
|
|
|
|
appendToLog($('<pre>').text(`${err.stack || err}`));
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
$msg.append(' done');
|
|
|
|
await incrementBar();
|
2021-04-22 07:10:19 +02:00
|
|
|
}
|
2021-06-09 06:34:33 +02:00
|
|
|
$progressArea.remove();
|
2012-10-27 18:05:26 +02:00
|
|
|
|
2021-05-10 00:45:43 +02:00
|
|
|
const grep = getURLParameter('grep');
|
|
|
|
if (grep != null) {
|
|
|
|
mocha.grep(grep);
|
|
|
|
}
|
|
|
|
const runner = mocha.run();
|
|
|
|
customRunner(runner);
|
|
|
|
})());
|