'use strict'; // $(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 () => { const stringifyException = (exception) => { let err = exception.stack || exception.toString(); // FF / Opera do not add the message if (!~err.indexOf(exception.message)) { err = `${exception.message}\n${err}`; } // <=IE7 stringifies to [Object Error]. Since it can be overloaded, we // check for the result of the stringifying. if (err === '[object Error]') err = exception.message; // Safari doesn't give you a stack. Let's at least provide a source line. if (!exception.stack && exception.sourceURL && exception.line !== undefined) { err += `\n(${exception.sourceURL}:${exception.line})`; } return err; }; const customRunner = (runner) => { const stats = {suites: 0, tests: 0, passes: 0, pending: 0, failures: 0}; let level = 0; if (!runner) return; // 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; }); runner.on('start', () => { stats.start = new Date(); }); runner.on('suite', (suite) => { if (suite.root) return; autoscroll($('#mocha-report .suite').last()[0]); stats.suites++; append(suite.title); level++; }); runner.on('suite end', (suite) => { if (suite.root) return; level--; if (level === 0) { append(''); } }); // max time a test is allowed to run // TODO this should be lowered once timeslider_revision.js is faster let killTimeout; runner.on('test end', () => { autoscroll($('#mocha-report .test').last()[0]); stats.tests++; }); runner.on('pass', (test) => { if (killTimeout) clearTimeout(killTimeout); killTimeout = setTimeout(() => { append('FINISHED - [red]no test started since 5 minutes, tests stopped[clear]'); }, 60000 * 5); const medium = test.slow() / 2; test.speed = test.duration > test.slow() ? 'slow' : test.duration > medium ? 'medium' : 'fast'; stats.passes++; append(`-> [green]PASSED[clear] : ${test.title} ${test.duration} ms`); }); runner.on('fail', (test, err) => { if (killTimeout) clearTimeout(killTimeout); killTimeout = setTimeout(() => { append('FINISHED - [red]no test started since 5 minutes, tests stopped[clear]'); }, 60000 * 5); stats.failures++; test.err = err; append(`-> [red]FAILED[clear] : ${test.title} ${stringifyException(test.err)}`); }); runner.on('pending', (test) => { if (killTimeout) clearTimeout(killTimeout); killTimeout = setTimeout(() => { append('FINISHED - [red]no test started since 5 minutes, tests stopped[clear]'); }, 60000 * 5); stats.pending++; append(`-> [yellow]PENDING[clear]: ${test.title}`); }); const $console = $('#console'); const append = (text) => { // Indent each line. const lines = text.split('\n').map((line) => ' '.repeat(level * 2) + line); $console.append(document.createTextNode(`${lines.join('\n')}\n`)); }; 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); // chrome < 57 does not like this .toString().padStart('2', '0'); const seconds = Math.round((stats.duration / 1000) % 60); if (stats.tests === total) { append(`FINISHED - ${stats.passes} tests passed, ${stats.failures} tests failed, ` + `${stats.pending} pending, duration: ${minutes}:${seconds}`); } else if (stats.tests > total) { 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'); } else { 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'); } }); }; const getURLParameter = (name) => (new URLSearchParams(location.search)).get(name); const absUrl = (url) => new URL(url, window.location.href).href; require.setRootURI(absUrl('../../javascripts/src')); require.setLibraryURI(absUrl('../../javascripts/lib')); require.setGlobalKeyPath('require'); const Split = require('split-grid/dist/split-grid.min'); new Split({ columnGutters: [{ track: 1, element: document.getElementById('separator'), }], }); // 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(); 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. ]; 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)) { defs[path] = function (require, exports, module) { 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) { if (typeof window[fn] !== 'function') continue; // 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); const $log = $('
'); 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 = $('
').css('white-space', 'nowrap').append(msg).appendTo($log); if (scrolledToBottom) $log[0].scrollTop = $log[0].scrollHeight; return $msg; }; const $bar = $(''); 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 = $('
') .css({'display': 'flex', 'flex-direction': 'column', 'height': '100%'}) .append($('
').css({flex: '1 0 0'})) .append($('
') .css({'flex': '0 0 auto', 'font-weight': 'bold'}) .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) { $bar.attr({value: 0, max: specs.length * 2}); await incrementBar(0); } const makeDesc = (spec) => `${spec .replace(/^ep_etherpad-lite\/tests\/frontend\/specs\//, ' ') .replace(/^([^/ ]*)\/static\/tests\/frontend\/specs\//, '<$1> ')}.js`; 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($('').css('color', 'red').text(' FAILED')); appendToLog($('
').text(`${err.stack || err}`));
      throw err;
    }
    $msg.append(' done');
    await incrementBar();
  }));
  require.setGlobalKeyPath('require');
  delete window.testRunnerRequire;
  for (const spec of specs) {
    const desc = makeDesc(spec);
    const $msg = appendToLog(`Executing ${desc}...`);
    try {
      describe(desc, function () {
        for (const [fn, args] of mochaCalls.get(spec)) window[fn](...args);
      });
    } catch (err) {
      $msg.append($('').css('color', 'red').text(' FAILED'));
      appendToLog($('
').text(`${err.stack || err}`));
      throw err;
    }
    $msg.append(' done');
    await incrementBar();
  }
  $progressArea.remove();

  const grep = getURLParameter('grep');
  if (grep != null) {
    mocha.grep(grep);
  }
  const runner = mocha.run();
  customRunner(runner);
})());