diff --git a/.travis.yml b/.travis.yml index fd750adf3..7b0ed03ae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,6 +33,8 @@ jobs: - if: fork = false name: "Test the Frontend" install: + #FIXME + - "sed 's/\"loglevel\": \"INFO\",/\"loglevel\": \"WARN\",/g' settings.json.template > settings.json" - "tests/frontend/travis/sauce_tunnel.sh" - "bin/installDeps.sh" - "export GIT_HASH=$(git rev-parse --verify --short HEAD)" diff --git a/tests/frontend/helper.js b/tests/frontend/helper.js index 80c6e803f..31c93374d 100644 --- a/tests/frontend/helper.js +++ b/tests/frontend/helper.js @@ -1,11 +1,9 @@ var helper = {}; (function(){ - var $iframeContainer, $iframe, jsLibraries = {}; + var $iframe, jsLibraries = {}; helper.init = function(cb){ - $iframeContainer = $("#iframe-container"); - $.get('/static/js/jquery.js').done(function(code){ // make sure we don't override existing jquery jsLibraries["jquery"] = "if(typeof $ === 'undefined') {\n" + code + "\n}"; @@ -90,6 +88,11 @@ var helper = {}; } helper.evtType = evtType; + // @todo needs fixing asap + // newPad occasionally timeouts, might be a problem with ready/onload code during page setup + // This ensures that tests run regardless of this problem + helper.retry = 0 + helper.newPad = function(cb, padName){ //build opts object var opts = {clearCookies: true} @@ -109,6 +112,9 @@ var helper = {}; helper.clearSessionCookies(); } + // needed for retry + let origPadName = padName; + if(!padName) padName = "FRONTEND_TEST_" + helper.randomString(20); $iframe = $(""); @@ -116,32 +122,36 @@ var helper = {}; //clean up inner iframe references helper.padChrome$ = helper.padOuter$ = helper.padInner$ = null; - //clean up iframes properly to prevent IE from memoryleaking - $iframeContainer.find("iframe").purgeFrame().done(function(){ - $iframeContainer.append($iframe); - $iframe.one('load', function(){ - helper.padChrome$ = getFrameJQuery( $('#iframe-container iframe')); - if (opts.clearCookies) { - helper.clearPadPrefCookie(); - } - if (opts.padPrefs) { - helper.setPadPrefCookie(opts.padPrefs); - } - helper.waitFor(function(){ - return !$iframe.contents().find("#editorloadingbox").is(":visible"); - }, 50000).done(function(){ - helper.padOuter$ = getFrameJQuery(helper.padChrome$('iframe[name="ace_outer"]')); - helper.padInner$ = getFrameJQuery( helper.padOuter$('iframe[name="ace_inner"]')); + //remove old iframe + $("#iframe-container iframe").remove(); + //set new iframe + $("#iframe-container").append($iframe); + $iframe.one('load', function(){ + helper.padChrome$ = getFrameJQuery($('#iframe-container iframe')); + if (opts.clearCookies) { + helper.clearPadPrefCookie(); + } + if (opts.padPrefs) { + helper.setPadPrefCookie(opts.padPrefs); + } + helper.waitFor(function(){ + return !$iframe.contents().find("#editorloadingbox").is(":visible"); + }, 10000).done(function(){ + helper.padOuter$ = getFrameJQuery(helper.padChrome$('iframe[name="ace_outer"]')); + helper.padInner$ = getFrameJQuery( helper.padOuter$('iframe[name="ace_inner"]')); - //disable all animations, this makes tests faster and easier - helper.padChrome$.fx.off = true; - helper.padOuter$.fx.off = true; - helper.padInner$.fx.off = true; + //disable all animations, this makes tests faster and easier + helper.padChrome$.fx.off = true; + helper.padOuter$.fx.off = true; + helper.padInner$.fx.off = true; - opts.cb(); - }).fail(function(){ + opts.cb(); + }).fail(function(){ + if (helper.retry > 3) { throw new Error("Pad never loaded"); - }); + } + helper.retry++; + helper.newPad(cb,origPadName); }); }); diff --git a/tests/frontend/index.html b/tests/frontend/index.html index 81b2a710a..d828e851c 100644 --- a/tests/frontend/index.html +++ b/tests/frontend/index.html @@ -18,7 +18,6 @@ - diff --git a/tests/frontend/lib/jquery.iframe.js b/tests/frontend/lib/jquery.iframe.js deleted file mode 100644 index 604ae1bc2..000000000 --- a/tests/frontend/lib/jquery.iframe.js +++ /dev/null @@ -1,40 +0,0 @@ -//copied from http://stackoverflow.com/questions/8407946/is-it-possible-to-use-iframes-in-ie-without-memory-leaks -(function($) { - $.fn.purgeFrame = function() { - var deferred; - var browser = bowser; - - if (browser.msie && parseFloat(browser.version, 10) < 9) { - deferred = purge(this); - } else { - this.remove(); - deferred = $.Deferred(); - deferred.resolve(); - } - - return deferred; - }; - - function purge($frame) { - var sem = $frame.length - , deferred = $.Deferred(); - - $frame.load(function() { - var frame = this; - frame.contentWindow.document.innerHTML = ''; - - sem -= 1; - if (sem <= 0) { - $frame.remove(); - deferred.resolve(); - } - }); - $frame.attr('src', 'about:blank'); - - if ($frame.length === 0) { - deferred.resolve(); - } - - return deferred.promise(); - } -})(jQuery); diff --git a/tests/frontend/runner.js b/tests/frontend/runner.js index e9eedc646..0ab380fb7 100644 --- a/tests/frontend/runner.js +++ b/tests/frontend/runner.js @@ -21,19 +21,15 @@ $(function(){ } function CustomRunner(runner) { - var self = this - , stats = this.stats = { suites: 0, tests: 0, passes: 0, pending: 0, failures: 0 } - , failures = this.failures = []; + var stats = { suites: 0, tests: 0, passes: 0, pending: 0, failures: 0 }; if (!runner) return; - this.runner = runner; runner.on('start', function(){ stats.start = new Date; }); runner.on('suite', function(suite){ - stats.suites = stats.suites || 0; suite.root || stats.suites++; if (suite.root) return; append(suite.title); @@ -50,31 +46,23 @@ $(function(){ }); // Scroll down test display after each test - mocha = $('#mocha')[0]; + let mochaEl = $('#mocha')[0]; runner.on('test', function(){ - mocha.scrollTop = mocha.scrollHeight; + mochaEl.scrollTop = mochaEl.scrollHeight; }); + // max time a test is allowed to run + // TODO this should be lowered once timeslider_revision.js is faster var killTimeout; - runner.on('test end', function(test){ - stats.tests = stats.tests || 0; + runner.on('test end', function(){ stats.tests++; - if ('passed' == test.state) { - append("->","[green]PASSED[clear] :", test.title," ",test.duration,"ms"); - } else if (test.pending) { - append("->","[yellow]PENDING[clear]:", test.title); - } else { - append("->","[red]FAILED[clear] :", test.title, stringifyException(test.err)); - } + }); + runner.on('pass', function(test){ if(killTimeout) clearTimeout(killTimeout); killTimeout = setTimeout(function(){ append("FINISHED - [red]no test started since 3 minutes, tests stopped[clear]"); }, 60000 * 3); - }); - - runner.on('pass', function(test){ - stats.passes = stats.passes || 0; var medium = test.slow() / 2; test.speed = test.duration > test.slow() @@ -84,22 +72,28 @@ $(function(){ : 'fast'; stats.passes++; + append("->","[green]PASSED[clear] :", test.title," ",test.duration,"ms"); }); runner.on('fail', function(test, err){ - stats.failures = stats.failures || 0; + if(killTimeout) clearTimeout(killTimeout); + killTimeout = setTimeout(function(){ + append("FINISHED - [red]no test started since 3 minutes, tests stopped[clear]"); + }, 60000 * 3); + stats.failures++; test.err = err; - failures.push(test); + append("->","[red]FAILED[clear] :", test.title, stringifyException(test.err)); }); - runner.on('end', function(){ - stats.end = new Date; - stats.duration = new Date - stats.start; - }); + runner.on('pending', function(test){ + if(killTimeout) clearTimeout(killTimeout); + killTimeout = setTimeout(function(){ + append("FINISHED - [red]no test started since 3 minutes, tests stopped[clear]"); + }, 60000 * 3); - runner.on('pending', function(){ stats.pending++; + append("->","[yellow]PENDING[clear]:", test.title); }); var $console = $("#console"); @@ -133,11 +127,19 @@ $(function(){ var total = runner.total; runner.on('end', function(){ - if(stats.tests >= total){ - var minutes = Math.floor(stats.duration / 1000 / 60); - var seconds = Math.round((stats.duration / 1000) % 60); - - append("FINISHED -", stats.passes, "tests passed,", stats.failures, "tests failed, duration: " + minutes + ":" + seconds); + stats.end = new Date; + stats.duration = stats.end - stats.start; + var minutes = Math.floor(stats.duration / 1000 / 60); + var seconds = Math.round((stats.duration / 1000) % 60) // chrome < 57 does not like this .toString().padStart("2","0"); + 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"); } }); } diff --git a/tests/frontend/travis/remote_runner.js b/tests/frontend/travis/remote_runner.js index df50a21d7..0f1ac699f 100644 --- a/tests/frontend/travis/remote_runner.js +++ b/tests/frontend/travis/remote_runner.js @@ -25,35 +25,41 @@ var sauceTestWorker = async.queue(function (testSettings, callback) { console.log("Remote sauce test '" + name + "' started! " + url); //tear down the test excecution - var stopSauce = function(success){ - getStatusInterval && clearInterval(getStatusInterval); + var stopSauce = function(success,timesup){ + clearInterval(getStatusInterval); clearTimeout(timeout); - browser.quit(); + browser.quit(function(){ + if(!success){ + allTestsPassed = false; + } - if(!success){ - allTestsPassed = false; - } + // 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. + var testResult = knownConsoleText.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(function(line){ + return "[" + testSettings.browserName + " " + testSettings.platform + (testSettings.version === "" ? '' : (" " + testSettings.version)) + "] " + line; + }).join("\n"); - var testResult = knownConsoleText.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(function(line){ - return "[" + testSettings.browserName + " " + testSettings.platform + (testSettings.version === "" ? '' : (" " + testSettings.version)) + "] " + line; - }).join("\n"); + console.log(testResult); + if (timesup) { + console.log("[" + testSettings.browserName + " " + testSettings.platform + (testSettings.version === "" ? '' : (" " + testSettings.version)) + "] allowed test duration exceeded"); + } + console.log("Remote sauce test '" + name + "' finished! " + url); - console.log(testResult); - console.log("Remote sauce test '" + name + "' finished! " + url); - - callback(); + callback(); + }); } /** - * timeout for the case the test hangs + * timeout if a test hangs or the job exceeds 9.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 */ var timeout = setTimeout(function(){ - stopSauce(false); - }, 1200000 * 10); + stopSauce(false,true); + }, 570000); // travis timeout is 10 minutes, set this to a slightly lower value var knownConsoleText = ""; var getStatusInterval = setInterval(function(){ @@ -64,11 +70,13 @@ var sauceTestWorker = async.queue(function (testSettings, callback) { knownConsoleText = consoleText; if(knownConsoleText.indexOf("FINISHED") > 0){ - let match = knownConsoleText.match(/FINISHED - ([0-9]+) tests passed, ([0-9]+) tests failed/); - if (match[2] && match[2] == 0){ + let match = knownConsoleText.match(/FINISHED.*([0-9]+) tests passed, ([0-9]+) tests failed/); + // finished without failures + if (match[2] && match[2] == '0'){ stopSauce(true); - } - else { + + // finished but some tests did not return or some tests failed + } else { stopSauce(false); } } @@ -128,8 +136,6 @@ sauceTestWorker.push({ , 'version' : '78.0' }); -sauceTestWorker.drain = function() { - setTimeout(function(){ - process.exit(allTestsPassed ? 0 : 1); - }, 3000); -} +sauceTestWorker.drain(function() { + process.exit(allTestsPassed ? 0 : 1); +}); diff --git a/tests/frontend/travis/runner.sh b/tests/frontend/travis/runner.sh index 2a4d76bd2..ffc6bbd5b 100755 --- a/tests/frontend/travis/runner.sh +++ b/tests/frontend/travis/runner.sh @@ -16,7 +16,7 @@ cd "${MY_DIR}/../../../" # This is possible because the "install" section of .travis.yml already contains # a call to bin/installDeps.sh echo "Running Etherpad directly, assuming bin/installDeps.sh has already been run" -node node_modules/ep_etherpad-lite/node/server.js "${@}" > /dev/null & +node node_modules/ep_etherpad-lite/node/server.js "${@}" & echo "Now I will try for 15 seconds to connect to Etherpad on http://localhost:9001" @@ -30,9 +30,6 @@ echo "Now I will try for 15 seconds to connect to Etherpad on http://localhost:9 echo "Successfully connected to Etherpad on http://localhost:9001" -# just in case, let's wait for another second before going on -sleep 1 - # On the Travis VM, remote_runner.js is found at # /home/travis/build/ether/[secure]/tests/frontend/travis/remote_runner.js # which is the same directory that contains this script. @@ -46,8 +43,6 @@ echo "Now starting the remote runner" node remote_runner.js exit_code=$? -kill $! kill $(cat /tmp/sauce.pid) -sleep 30 exit $exit_code