Switch to chrome headless mode instead of phantomjs.

This commit is contained in:
Guo Xiang Tan 2017-12-19 15:59:41 +08:00
parent 30ddc1f222
commit 6a4f391e38
13 changed files with 284 additions and 721 deletions

View File

@ -14,6 +14,3 @@ brew 'postgresql'
# install the Redis datastore
brew 'redis'
# install headless Javascript testing library
brew 'phantomjs'

View File

@ -38,7 +38,7 @@ To get your Ubuntu 16.04 LTS install up and running to develop Discourse and Dis
nvm install node
nvm alias default node
npm install -g svgo phantomjs-prebuilt
npm install -g svgo
If everything goes alright, let's clone Discourse and start hacking:
@ -54,10 +54,10 @@ If everything goes alright, let's clone Discourse and start hacking:
# time to create the database and run migrations
bundle exec rake db:create db:migrate
RAILS_ENV=test bundle exec rake db:create db:migrate
# run the specs (optional)
bundle exec rake autospec # CTRL + C to stop
# launch discourse
bundle exec rails s -b 0.0.0.0 # open browser on http://localhost:3000 and you should see Discourse

View File

@ -183,11 +183,11 @@ You should not need to alter `/usr/local/var/postgres/pg_hba.conf`
That's about it.
## PhantomJS
## Google Chrome 59+
Homebrew loves you.
Chrome is used for running QUnit tests in headless mode.
brew install phantomjs
Download from https://www.google.com/chrome/index.html
## ImageMagick

View File

@ -24,10 +24,10 @@ module Autospec
require "socket"
class PhantomJsNotInstalled < StandardError; end
class ChromeNotInstalled < StandardError; end
def initialize
ensure_phantomjs_is_installed
ensure_chrome_is_installed
end
def start
@ -66,7 +66,7 @@ module Autospec
end
end
cmd = "phantomjs #{Rails.root}/lib/autospec/run-qunit.js \"#{qunit_url}\""
cmd = "node #{Rails.root}/lib/autospec/run-qunit.js \"#{qunit_url}\""
@pid = Process.spawn(cmd)
_, status = Process.wait2(@pid)
@ -96,7 +96,6 @@ module Autospec
end
def stop
# kill phantomjs first
abort
stop_rails_server
@running = false
@ -104,8 +103,12 @@ module Autospec
private
def ensure_phantomjs_is_installed
raise PhantomJsNotInstalled.new unless system("command -v phantomjs >/dev/null;")
def ensure_chrome_is_installed
raise ChromeNotInstalled.new unless system("command -v google-chrome >/dev/null;")
if Gem::Version.new(`$(command -v google-chrome) --version`.match(/[\d\.]+/)[0]) < Gem::Version.new("59")
raise "Chrome 59 or higher is required"
end
end
def port_available?(port)

View File

@ -1,182 +1,182 @@
/*jshint devel:true, phantom:true */
/*globals QUnit ANSI */
// Chrome QUnit Test Runner
// Author: David Taylor
// Requires chrome-launcher and chrome-remote-interface from npm
// An up-to-date version of chrome is also required
// THIS FILE IS CALLED BY "qunit_runner.rb" IN AUTOSPEC
/* globals Promise */
var system = require("system"),
args = phantom.args;
var args = process.argv.slice(2);
if (args === undefined) {
args = system.args;
args.shift();
if (args.length < 1 || args.length > 2) {
console.log("Usage: node run-qunit.js <URL> <timeout>");
process.exit(1);
}
if (args.length !== 1) {
console.log("Usage: " + phantom.scriptName + " <URL>");
phantom.exit(1);
}
const chromeLauncher = require('chrome-launcher');
const CDP = require('chrome-remote-interface');
const fs = require('fs');
const QUNIT_RESULT = "./tmp/qunit_result";
var fs = require('fs'),
page = require('webpage').create(),
QUNIT_RESULT = "./tmp/qunit_result";
(async () => {
await fs.stat(QUNIT_RESULT, (err, stats) => {
if (stats && stats.isFile()) fs.unlink(QUNIT_RESULT);
});
})();
if (fs.exists(QUNIT_RESULT) && fs.isFile(QUNIT_RESULT)) { fs.remove(QUNIT_RESULT); }
(function() {
page.onConsoleMessage = function (message) {
// filter out Ember's debug messages
if (message.slice(0, 8) === "WARNING:") { return; }
if (message.slice(0, 6) === "DEBUG:") { return; }
console.log(message);
};
page.onCallback = function (message) {
// write to the result file
if (message.slice(0, 5) === "FAIL:") {
fs.write(QUNIT_RESULT, message.slice(6) + "\n", "a");
function launchChrome() {
return chromeLauncher.launch({
chromeFlags: [
'--disable-gpu',
'--headless',
'--no-sandbox'
]
});
}
// forward the message to the standard output
if (message.slice(0, 6) === "PRINT:") { system.stdout.write(message.slice(7)); }
};
page.start = new Date();
launchChrome().then(chrome => {
CDP({
port: chrome.port
}).then(protocol => {
const {Page, Runtime} = protocol;
Promise.all([Page.enable(), Runtime.enable()]).then(()=>{
// -----------------------------------WARNING --------------------------------------
// calling "console.log" BELOW this line will go through the "page.onConsoleMessage"
// -----------------------------------WARNING --------------------------------------
page.open(args[0], function (status) {
if (status !== "success") {
console.log("\nNO NETWORK :(\n");
phantom.exit(1);
} else {
console.log("QUnit loaded in " + (new Date() - page.start) + " ms");
Runtime.consoleAPICalled((response) => {
const message = response['args'][0].value;
page.evaluate(colorizer);
page.evaluate(logQUnit);
// wait up to 600 seconds for QUnit to finish
var timeout = 600 * 1000,
start = Date.now();
var interval = setInterval(function() {
if (Date.now() - start > timeout) {
console.error("\nTIME OUT :(\n");
phantom.exit(1);
} else {
var qunitResult = page.evaluate(function() { return window.qunitResult; });
if (qunitResult) {
clearInterval(interval);
if (qunitResult.failed > 0) {
phantom.exit(1);
// If it's a simple test result, write without newline
if (message === "." || message === "F") {
process.stdout.write(message);
} else if (message.startsWith("AUTOSPEC:")) {
fs.appendFileSync(QUNIT_RESULT, `${message.slice(10)}\n`);
} else {
phantom.exit(0);
console.log(message);
}
}
}
}, 250);
}
});
});
// https://github.com/jquery/qunit/pull/470
function colorizer() {
window.ANSI = {
colorMap: {
"red": "\u001b[31m",
"green": "\u001b[32m",
"blue": "\u001b[34m",
"end": "\u001b[0m"
},
highlightMap: {
"red": "\u001b[41m\u001b[37m", // change 37 to 30 for black text
"green": "\u001b[42m\u001b[30m",
"blue": "\u001b[44m\u001b[37m",
"end": "\u001b[0m"
},
Page.navigate({
url: args[0]
});
highlight: function (text, color) {
var colorCode = this.highlightMap[color],
colorEnd = this.highlightMap.end;
Page.loadEventFired(() => {
return colorCode + text + colorEnd;
},
Runtime.evaluate({
expression: `(${qunit_script})()`
}).then(() => {
const timeout = parseInt(args[1] || 300000, 10);
var start = Date.now();
colorize: function (text, color) {
var colorCode = this.colorMap[color],
colorEnd = this.colorMap.end;
var interval = setInterval(() => {
if (Date.now() > start + timeout) {
console.error("Tests timed out");
return colorCode + text + colorEnd;
}
protocol.close();
chrome.kill();
process.exit(124);
} else {
Runtime.evaluate({
expression: `(${check_script})()`
}).then(numFails => {
if (numFails.result.type !== 'undefined') {
clearInterval(interval);
protocol.close();
chrome.kill();
if (numFails.result.value > 0) {
process.exit(1);
} else {
process.exit();
}
}
}).catch(error);
}
}, 250);
}).catch(error(1));
});
}).catch(error(3));
}).catch(error(4));
}).catch(error(5));
})();
function error(code){
return function(){
console.log("A promise failed to resolve code:"+code);
process.exit(1);
};
}
// The following functions are converted to strings
// And then sent to chrome to be evalaluated
function logQUnit() {
// keep track of error messages
var errors = {};
var moduleErrors = [];
var testErrors = [];
var assertionErrors = [];
QUnit.begin(function () {
console.log("BEGIN");
});
console.log("\nRunning: " + JSON.stringify(QUnit.urlParams) + "\n");
QUnit.log(function (context) {
if (!context.result) {
var module = context.module,
test = context.name;
QUnit.config.testTimeout = 10000;
var assertion = {
message: context.message,
expected: context.expected,
actual: context.actual
};
if (!errors[module]) { errors[module] = {}; }
if (!errors[module][test]) { errors[module][test] = []; }
errors[module][test].push(assertion);
window.callPhantom("FAIL: " + context.module + ":::" + context.testId + ":::" + context.name);
QUnit.moduleDone(function(context) {
if (context.failed) {
var msg = "Module Failed: " + context.name + "\n" + testErrors.join("\n");
moduleErrors.push(msg);
testErrors = [];
}
});
QUnit.testDone(function (context) {
if (context.failed > 0) {
window.callPhantom("PRINT: " + ANSI.colorize("F", "red"));
QUnit.testDone(function(context) {
if (context.failed) {
var msg = " Test Failed: " + context.name + assertionErrors.join(" ");
console.log(`AUTOSPEC: ${context.module}:::${context.testId}:::${context.name}`);
testErrors.push(msg);
assertionErrors = [];
console.log("F");
} else {
window.callPhantom("PRINT: " + ANSI.colorize(".", "green"));
console.log(".");
}
});
QUnit.done(function (context) {
QUnit.log(function(context) {
if (context.result) { return; }
var msg = "\n Assertion Failed:";
if (context.message) {
msg += " " + context.message;
}
if (context.expected) {
msg += "\n Expected: " + context.expected + ", Actual: " + context.actual;
}
assertionErrors.push(msg);
});
QUnit.done(function(context) {
console.log("\n");
// display failures
if (Object.keys(errors).length > 0) {
console.log("Failures:\n");
for (var m in errors) {
var module = errors[m];
console.log("Module Failed: " + ANSI.highlight(m, "red"));
for (var t in module) {
var test = module[t];
console.log(" Test Failed: " + t);
for (var a = 0; a < test.length; a++) {
var assertion = test[a];
console.log(" Assertion Failed: " + (assertion.message || ""));
if (assertion.expected) {
console.log(" Expected: " + assertion.expected);
console.log(" Actual: " + assertion.actual);
}
}
}
if (moduleErrors.length > 0) {
for (var idx=0; idx<moduleErrors.length; idx++) {
console.error(moduleErrors[idx]+"\n");
}
}
// display summary
console.log("\n");
console.log("Finished in " + (context.runtime / 1000) + " seconds");
var color = context.failed > 0 ? "red" : "green";
console.log(ANSI.colorize(context.total + " examples, " + context.failed + " failures", color));
// we're done
window.qunitResult = context;
var stats = [
"Time: " + context.runtime + "ms",
"Total: " + context.total,
"Passed: " + context.passed,
"Failed: " + context.failed
];
console.log(stats.join(", "));
window.qunitDone = context;
});
}
const qunit_script = logQUnit.toString();
function check() {
if(window.qunitDone){
return window.qunitDone.failed;
}
}
const check_script = check.toString();

View File

@ -5,10 +5,20 @@ task "qunit:test", [:timeout, :qunit_path] => :environment do |_, args|
require "rack"
require "socket"
unless %x{which phantomjs > /dev/null 2>&1} || ENV["USE_CHROME"]
abort "PhantomJS is not installed. Download from http://phantomjs.org"
unless system("command -v google-chrome >/dev/null;")
abort "Chrome is not installed. Download from https://www.google.com/chrome/browser/desktop/index.html"
end
if Gem::Version.new(`$(command -v google-chrome) --version`.match(/[\d\.]+/)[0]) < Gem::Version.new("59")
abort "Chrome 59 or higher is required to run tests in headless mode."
end
unless system("command -v yarn >/dev/null;")
abort "Yarn is not installed. Download from https://yarnpkg.com/lang/en/docs/install/"
end
system("yarn install --dev")
# ensure we have this port available
def port_available?(port)
server = TCPServer.open port
@ -36,13 +46,7 @@ task "qunit:test", [:timeout, :qunit_path] => :environment do |_, args|
success = true
test_path = "#{Rails.root}/vendor/assets/javascripts"
qunit_path = args[:qunit_path] || "/qunit"
if ENV["USE_CHROME"]
cmd = "node #{test_path}/run-qunit-chrome.js http://localhost:#{port}#{qunit_path}"
else
cmd = "phantomjs #{test_path}/run-qunit.js http://localhost:#{port}#{qunit_path}"
end
cmd = "node #{test_path}/run-qunit.js http://localhost:#{port}#{qunit_path}"
options = {}
%w{module filter qunit_skip_core qunit_single_plugin}.each do |arg|

View File

@ -1,8 +1,12 @@
desc "run phantomjs based smoke tests on current build"
desc "run chrome headless smoke tests on current build"
task "smoke:test" do
phantom_path = File.expand_path('~/phantomjs/bin/phantomjs')
phantom_path = nil unless File.exists?(phantom_path)
phantom_path = phantom_path || 'phantomjs'
unless system("command -v google-chrome >/dev/null;")
abort "Chrome is not installed. Download from https://www.google.com/chrome/browser/desktop/index.html"
end
if Gem::Version.new(`$(command -v google-chrome) --version`.match(/[\d\.]+/)[0]) < Gem::Version.new("59")
abort "Chrome 59 or higher is required to run smoke tests in headless mode."
end
url = ENV["URL"]
if !url
@ -32,12 +36,7 @@ task "smoke:test" do
results = ""
command =
if ENV["USE_CHROME"]
"node #{Rails.root}/test/smoke_test.js #{url}"
else
"#{phantom_path} #{Rails.root}/spec/phantom_js/smoke_test.js #{url}"
end
"node #{Rails.root}/test/smoke_test.js #{url}"
IO.popen(command).each do |line|
puts line

View File

@ -1,307 +0,0 @@
/*global phantom:true */
console.log("Starting Discourse Smoke Test");
var system = require("system");
if (system.args.length !== 2) {
console.log("Expecting: phantomjs {smoke_test.js} {url}");
phantom.exit(1);
}
var TIMEOUT = 25000;
var page = require("webpage").create();
if (system.env["AUTH_USER"] && system.env["AUTH_PASSWORD"]) {
page.settings.userName = system.env["AUTH_USER"];
page.settings.password = system.env["AUTH_PASSWORD"];
}
page.viewportSize = {
width: 1366,
height: 768
};
// In the browser, when the cookies are disabled, it also disables the localStorage
// Here, we're mocking that behavior and making sure the application doesn't blow up
page.onInitialized = function() {
page.evaluate(function() {
localStorage["disableLocalStorage"] = true;
});
};
page.onConsoleMessage = function(msg) {
console.log(msg);
};
page.waitFor = function(desc, fn, cb) {
var start = +new Date();
var check = function() {
var r;
try { r = page.evaluate(fn); } catch (err) { }
var diff = (+new Date()) - start;
if (r) {
console.log("PASSED: " + desc + " - " + diff + "ms");
cb(true);
} else {
if (diff > TIMEOUT) {
console.log("FAILED: " + desc + " - " + diff + "ms");
page.render('/tmp/failed.png');
console.log('Content:' + page.content);
cb(false);
} else {
setTimeout(check, 25);
}
}
};
check();
};
var actions = [];
function test(desc, fn) {
actions.push({ test: fn, desc: desc });
};
// function wait(delay) {
// actions.push({ wait: delay });
// }
function exec(desc, fn) {
actions.push({ exec: fn, desc: desc });
};
function execAsync(desc, delay, fn) {
actions.push({ execAsync: fn, delay: delay, desc: desc });
};
// function upload(input, path) {
// actions.push({ upload: path, input: input });
// };
// function screenshot(filename) {
// actions.push({ screenshot: filename });
// }
function run() {
var allPassed = true;
var done = function() {
console.log(allPassed ? "ALL PASSED" : "SMOKE TEST FAILED");
phantom.exit();
};
var performNextAction = function() {
if (!allPassed || actions.length === 0) {
done();
} else {
var action = actions[0];
actions = actions.splice(1);
if (action.test) {
page.waitFor(action.desc, action.test, function(success) {
allPassed = allPassed && success;
performNextAction();
});
} else if (action.exec) {
console.log("EXEC: " + action.desc);
page.evaluate(action.exec, system);
performNextAction();
} else if (action.execAsync) {
console.log("EXEC ASYNC: " + action.desc + " - " + action.delay + "ms");
setTimeout(function() {
page.evaluate(action.execAsync);
performNextAction();
}, action.delay);
} else if (action.upload) {
console.log("UPLOAD: " + action.upload);
page.uploadFile(action.input, action.upload);
performNextAction();
} else if (action.screenshot) {
console.log("SCREENSHOT: " + action.screenshot);
page.render(action.screenshot);
performNextAction();
} else if (action.wait) {
console.log("WAIT: " + action.wait + "ms");
setTimeout(function() {
performNextAction();
}, action.wait);
}
}
};
performNextAction();
};
var runTests = function() {
test("expect a log in button in the header", function() {
return $("header .login-button").length;
});
execAsync("go to latest page", 500, function(){
window.location = "/latest";
});
test("at least one topic shows up", function() {
return $(".topic-list tbody tr").length;
});
execAsync("go to categories page", 500, function(){
window.location = "/categories";
});
test("can see categories on the page", function() {
return $('.category-list').length;
});
execAsync("navigate to 1st topic", 500, function() {
$(".main-link a.title:first").click();
});
test("at least one post body", function() {
return $(".topic-post").length;
});
execAsync("click on the 1st user", 500, function() {
// remove the popup action for testing
$(".topic-meta-data a:first").data("ember-action", "");
$(".topic-meta-data a:first").focus().click();
});
test("user has details", function() {
return $("#user-card .names").length;
});
if (!system.env["READONLY_TESTS"]) {
exec("open login modal", function() {
$(".login-button").click();
});
test("login modal is open", function() {
return $(".login-modal").length;
});
exec("type in credentials & log in", function(system) {
$("#login-account-name").val(system.env['DISCOURSE_USERNAME'] || 'smoke_user').trigger("change");
$("#login-account-password").val(system.env["DISCOURSE_PASSWORD"] || 'P4ssw0rd').trigger("change");
$(".login-modal .btn-primary").click();
});
test("is logged in", function() {
return $(".current-user").length;
});
exec("go home", function() {
if ($('#site-logo').length) $('#site-logo').click();
if ($('#site-text-logo').length) $('#site-text-logo').click();
});
test("it shows a topic list", function() {
return $(".topic-list").length;
});
test('we have a create topic button', function() {
return $("#create-topic").length;
});
exec("open composer", function() {
$("#create-topic").click();
});
test('the editor is visible', function() {
return $(".d-editor").length;
});
exec("compose new topic", function() {
var date = " (" + (+new Date()) + ")",
title = "This is a new topic" + date,
post = "I can write a new topic inside the smoke test!" + date + "\n\n";
$("#reply-title").val(title).trigger("change");
$("#reply-control .d-editor-input").val(post).trigger("change");
$("#reply-control .d-editor-input").focus()[0].setSelectionRange(post.length, post.length);
});
test("updates preview", function() {
return $(".d-editor-preview p").length;
});
exec("open upload modal", function() {
$(".d-editor-button-bar .upload").click();
});
test("upload modal is open", function() {
return $("#filename-input").length;
});
// TODO: Looks like PhantomJS 2.0.0 has a bug with `uploadFile`
// which breaks this code.
// upload("#filename-input", "spec/fixtures/images/large & unoptimized.png");
// test("the file is inserted into the input", function() {
// return document.getElementById('filename-input').files.length
// });
// screenshot('/tmp/upload-modal.png');
//
// test("upload modal is open", function() {
// return document.querySelector("#filename-input");
// });
//
// exec("click upload button", function() {
// $(".modal .btn-primary").click();
// });
//
// test("image is uploaded", function() {
// return document.querySelector(".cooked img");
// });
exec("submit the topic", function() {
$("#reply-control .create").click();
});
test("topic is created", function() {
return $(".fancy-title").length;
});
exec("click reply button", function() {
$(".post-controls:first .create").click();
});
test("composer is open", function() {
return $("#reply-control .d-editor-input").length;
});
exec("compose reply", function() {
var post = "I can even write a reply inside the smoke test ;) (" + (+new Date()) + ")";
$("#reply-control .d-editor-input").val(post).trigger("change");
});
test("waiting for the preview", function() {
return $(".d-editor-preview").text().trim().indexOf("I can even write") === 0;
});
execAsync("submit the reply", 6000, function() {
$("#reply-control .create").click();
});
test("reply is created", function() {
return !document.querySelector(".saving-text")
&& $(".topic-post").length === 2;
});
}
run();
};
phantom.clearCookies();
page.open(system.args[1], function() {
page.evaluate(function() { localStorage.clear(); });
console.log("OPENED: " + system.args[1]);
runTests();
});

View File

@ -38,11 +38,10 @@ QUnit.test("Updating topic notification level", assert => {
"it should display the right notification level"
);
// TODO: tgxworld I can't figure out why the topic timeline doesn't show when
// running the tests in phantomjs
// ok(
// exists(".timeline-footer-controls .notifications-button .watching"),
// 'it should display the right notification level in topic timeline'
// );
assert.equal(
find(`.timeline-footer-controls .select-kit-header`).data().name,
'Watching',
'it should display the right notification level in topic timeline'
);
});
});

View File

@ -37,4 +37,4 @@ QUnit.test("userPath with BaseUri", assert => {
assert.equal(userPath(), '/forum/u');
assert.equal(userPath('eviltrout'), '/forum/u/eviltrout');
assert.equal(userPath('hp.json'), '/forum/u/hp.json');
});
});

View File

@ -2357,7 +2357,7 @@ return /******/ (function(modules) { // webpackBootstrap
$export.B = 16; // bind
$export.W = 32; // wrap
$export.U = 64; // safe
$export.R = 128; // real proto method for `library`
$export.R = 128; // real proto method for `library`
module.exports = $export;
/***/ }),
@ -62209,13 +62209,6 @@ return /******/ (function(modules) { // webpackBootstrap
"test": false,
"throws": false
},
"phantomjs": {
"console": true,
"exports": true,
"phantom": true,
"require": true,
"WebPage": true
},
"couch": {
"emit": false,
"exports": false,
@ -62843,4 +62836,4 @@ return /******/ (function(modules) { // webpackBootstrap
/***/ })
/******/ ])))
});
;
;

View File

@ -1,171 +0,0 @@
// Chrome QUnit Test Runner
// Author: David Taylor
// Requires chrome-launcher and chrome-remote-interface from npm
// An up-to-date version of chrome is also required
/* globals Promise */
var args = process.argv.slice(2);
if (args.length < 1 || args.length > 2) {
console.log("Usage: node run-qunit-chrome.js <URL> <timeout>");
process.exit(1);
}
const chromeLauncher = require('chrome-launcher');
const CDP = require('chrome-remote-interface');
(function() {
function launchChrome() {
return chromeLauncher.launch({
chromeFlags: [
'--disable-gpu',
'--headless',
'--no-sandbox'
]
});
}
launchChrome().then(chrome => {
CDP({
port: chrome.port
}).then(protocol => {
const {Page, Runtime} = protocol;
Promise.all([Page.enable(), Runtime.enable()]).then(()=>{
Runtime.consoleAPICalled((response) => {
const message = response['args'][0].value;
// If it's a simple test result, write without newline
if(message === "." || message === "F"){
process.stdout.write(message);
}else{
console.log(message);
}
});
Page.navigate({
url: args[0]
});
Page.loadEventFired(() => {
Runtime.evaluate({
expression: `(${qunit_script})()`
}).then(() => {
const timeout = parseInt(args[1] || 300000, 10);
var start = Date.now();
var interval = setInterval(() => {
if (Date.now() > start + timeout) {
console.error("Tests timed out");
protocol.close();
chrome.kill();
process.exit(124);
} else {
Runtime.evaluate({
expression: `(${check_script})()`
}).then(numFails => {
if (numFails.result.type !== 'undefined') {
clearInterval(interval);
protocol.close();
chrome.kill();
if (numFails.result.value > 0) {
process.exit(1);
} else {
process.exit();
}
}
}).catch(error);
}
}, 250);
}).catch(error(1));
});
}).catch(error(3));
}).catch(error(4));
}).catch(error(5));
})();
function error(code){
return function(){
console.log("A promise failed to resolve code:"+code);
process.exit(1);
};
}
// The following functions are converted to strings
// And then sent to chrome to be evalaluated
function logQUnit() {
var moduleErrors = [];
var testErrors = [];
var assertionErrors = [];
console.log("\nRunning: " + JSON.stringify(QUnit.urlParams) + "\n");
QUnit.config.testTimeout = 10000;
QUnit.moduleDone(function(context) {
if (context.failed) {
var msg = "Module Failed: " + context.name + "\n" + testErrors.join("\n");
moduleErrors.push(msg);
testErrors = [];
}
});
QUnit.testDone(function(context) {
if (context.failed) {
var msg = " Test Failed: " + context.name + assertionErrors.join(" ");
testErrors.push(msg);
assertionErrors = [];
console.log("F");
} else {
console.log(".");
}
});
QUnit.log(function(context) {
if (context.result) { return; }
var msg = "\n Assertion Failed:";
if (context.message) {
msg += " " + context.message;
}
if (context.expected) {
msg += "\n Expected: " + context.expected + ", Actual: " + context.actual;
}
assertionErrors.push(msg);
});
QUnit.done(function(context) {
console.log("\n");
if (moduleErrors.length > 0) {
for (var idx=0; idx<moduleErrors.length; idx++) {
console.error(moduleErrors[idx]+"\n");
}
}
var stats = [
"Time: " + context.runtime + "ms",
"Total: " + context.total,
"Passed: " + context.passed,
"Failed: " + context.failed
];
console.log(stats.join(", "));
window.qunitDone = context;
});
}
const qunit_script = logQUnit.toString();
function check() {
if(window.qunitDone){
return window.qunitDone.failed;
}
}
const check_script = check.toString();

View File

@ -1,66 +1,104 @@
// PhantomJS QUnit Test Runner
// Chrome QUnit Test Runner
// Author: David Taylor
// Requires chrome-launcher and chrome-remote-interface from npm
// An up-to-date version of chrome is also required
/*globals QUnit phantom*/
/* globals Promise */
var system = require("system"),
args = phantom.args;
if (args === undefined) {
args = system.args;
args.shift();
}
var args = process.argv.slice(2);
if (args.length < 1 || args.length > 2) {
console.log("Usage: " + phantom.scriptName + " <URL> <timeout>");
phantom.exit(1);
console.log("Usage: node run-qunit.js <URL> <timeout>");
process.exit(1);
}
var page = require("webpage").create();
const chromeLauncher = require('chrome-launcher');
const CDP = require('chrome-remote-interface');
page.onConsoleMessage = function(msg) {
if (msg.slice(0, 8) === "WARNING:") { return; }
if (msg.slice(0, 6) === "DEBUG:") { return; }
(function() {
console.log(msg);
};
function launchChrome() {
return chromeLauncher.launch({
chromeFlags: [
'--disable-gpu',
'--headless',
'--no-sandbox'
]
});
}
page.onCallback = function (message) {
// forward the message to the standard output
system.stdout.write(message);
};
launchChrome().then(chrome => {
CDP({
port: chrome.port
}).then(protocol => {
const {Page, Runtime} = protocol;
Promise.all([Page.enable(), Runtime.enable()]).then(()=>{
page.open(args[0], function(status) {
if (status !== "success") {
console.error("Unable to access network");
phantom.exit(1);
} else {
page.evaluate(logQUnit);
Runtime.consoleAPICalled((response) => {
const message = response['args'][0].value;
var timeout = parseInt(args[1] || 300000, 10),
start = Date.now();
var interval = setInterval(function() {
if (Date.now() > start + timeout) {
console.error("Tests timed out");
phantom.exit(124);
} else {
var qunitDone = page.evaluate(function() {
return window.qunitDone;
// If it's a simple test result, write without newline
if(message === "." || message === "F"){
process.stdout.write(message);
}else{
console.log(message);
}
});
if (qunitDone) {
clearInterval(interval);
if (qunitDone.failed > 0) {
phantom.exit(1);
} else {
phantom.exit();
}
}
}
}, 250);
}
});
Page.navigate({
url: args[0]
});
Page.loadEventFired(() => {
Runtime.evaluate({
expression: `(${qunit_script})()`
}).then(() => {
const timeout = parseInt(args[1] || 300000, 10);
var start = Date.now();
var interval = setInterval(() => {
if (Date.now() > start + timeout) {
console.error("Tests timed out");
protocol.close();
chrome.kill();
process.exit(124);
} else {
Runtime.evaluate({
expression: `(${check_script})()`
}).then(numFails => {
if (numFails.result.type !== 'undefined') {
clearInterval(interval);
protocol.close();
chrome.kill();
if (numFails.result.value > 0) {
process.exit(1);
} else {
process.exit();
}
}
}).catch(error);
}
}, 250);
}).catch(error(1));
});
}).catch(error(3));
}).catch(error(4));
}).catch(error(5));
})();
function error(code){
return function(){
console.log("A promise failed to resolve code:"+code);
process.exit(1);
};
}
// The following functions are converted to strings
// And then sent to chrome to be evalaluated
function logQUnit() {
var moduleErrors = [];
var testErrors = [];
@ -83,9 +121,9 @@ function logQUnit() {
var msg = " Test Failed: " + context.name + assertionErrors.join(" ");
testErrors.push(msg);
assertionErrors = [];
window.callPhantom("F");
console.log("F");
} else {
window.callPhantom(".");
console.log(".");
}
});
@ -123,3 +161,11 @@ function logQUnit() {
window.qunitDone = context;
});
}
const qunit_script = logQUnit.toString();
function check() {
if(window.qunitDone){
return window.qunitDone.failed;
}
}
const check_script = check.toString();