Tests: Improve QUnit test logging and various cleanup on helpers.

This commit is contained in:
Jürg Lehni 2016-01-28 02:21:34 +01:00
parent f7053689bc
commit e0429f44df
5 changed files with 215 additions and 194 deletions

View file

@ -13,16 +13,7 @@
var gulp = require('gulp'),
gulp_qunit = require('gulp-qunit'),
node_qunit = require('qunit'),
gutil = require('gulp-util'),
extend = require('extend'),
minimist = require('minimist');
// Support simple command line options to pass on to test:node, to display
// errors selectively, e.g.:
// gulp test:node --assertions
var options = minimist(process.argv.slice(2), {
boolean: true
});
gutil = require('gulp-util');
gulp.task('test', ['test:browser']);
@ -32,12 +23,11 @@ gulp.task('test:browser', ['minify:acorn'], function() {
});
gulp.task('test:node', ['minify:acorn'], function(callback) {
var name = 'node-qunit';
node_qunit.setup({
log: extend({ errors: true }, options)
});
// Use the correct working directory for tests:
process.chdir('./test');
// Deactivate all logging since we're doing our own directly to gutil.log()
// from helpers.js
node_qunit.setup({ log: {} });
node_qunit.run({
maxBlockDuration: 100 * 1000,
deps: [
@ -55,24 +45,7 @@ gulp.task('test:node', ['minify:acorn'], function(callback) {
// for the loading, which was requested above.
code: 'load.js'
}, function(err, stats) {
var result;
if (err) {
result = new gutil.PluginError(name, err);
} else {
// Imitate the way gulp-qunit formats results and errors.
var color = gutil.colors[stats.failed > 0 ? 'red' : 'green'];
gutil.log('Took ' + stats.runtime + ' ms to run ' +
gutil.colors.blue(stats.assertions) + ' tests. ' +
color(stats.passed + ' passed, ' + stats.failed + ' failed.'));
if (stats.failed > 0) {
err = 'QUnit assertions failed';
gutil.log(name + ': ' + gutil.colors.red('✖ ') + err);
result = new gutil.PluginError(name, err);
} else {
gutil.log(name + ': ' + gutil.colors.green('✔ ') +
'QUnit assertions all passed');
}
}
callback(result);
err = err || stats.failed > 0 && 'QUnit assertions failed';
callback(err && new gutil.PluginError('node-qunit', err));
});
});

View file

@ -42,7 +42,7 @@
"gulp": "^3.9.0",
"gulp-cached": "^1.1.0",
"gulp-jshint": "^2.0.0",
"gulp-prepro": "^2.1.0",
"gulp-prepro": "^2.2.0",
"gulp-qunit": "git://github.com/lehni/gulp-qunit.git#459c5603ceac460327a40dc89df6f19c786dc61b",
"gulp-rename": "^1.2.2",
"gulp-rimraf": "^0.2.0",
@ -56,8 +56,7 @@
"jshint": "2.8.x",
"jshint-summary": "^0.4.0",
"merge-stream": "^1.0.0",
"minimist": "^1.2.0",
"prepro": "^2.1.0",
"prepro": "^2.2.0",
"qunit": "^0.7.7",
"qunitjs": "^1.20.0",
"require-dir": "^0.3.0",

View file

@ -15,8 +15,52 @@ var isNode = typeof global === 'object',
if (isNode) {
root = global;
// Resemble.js needs the Image constructor this global.
// Resemble.js needs the Image constructor global.
global.Image = paper.window.Image;
// Handle logging to gulp directly from here, imitating the way gulp-qunit
// logs and formats results and errors:
var gutil = require('gulp-util'),
colors = gutil.colors,
done = false;
QUnit.log(function(details) {
if (!details.result) {
var lines = [
colors.red('Test failed') + ': ' + details.module + ': '
+ details.name
];
var line = 'Failed assertion: ' + (details.message || '');
if (details.expected !== undefined) {
line += ', expected: ' + details.expected + ', but was: '
+ details.actual;
}
lines.push(line);
if (details.source) {
lines = lines.concat(details.source.split(/\r\n|\n|\r/mg));
}
lines.forEach(function(line) {
gutil.log(line);
});
} else if (false) {
gutil.log(colors.green('Test succeeded') + ': ' + details.module
+ ': ' + details.name +': ' + (details.message || ''));
}
});
QUnit.done(function(details) {
if (done)
return;
var color = colors[details.failed > 0 ? 'red' : 'green'];
gutil.log('Took ' + details.runtime + 'ms to run '
+ colors.blue(details.total) + ' tests. ' + color(details.passed
+ ' passed, ' + details.failed + ' failed.'));
if (details.failed > 0) {
gutil.log('node-qunit: ' + gutil.colors.red('✖')
+ ' QUnit assertions failed');
} else {
gutil.log('node-qunit: ' + gutil.colors.green('✔')
+ ' QUnit assertions all passed');
}
done = true;
});
} else {
root = window;
// This is only required when running in the browser:
@ -26,20 +70,10 @@ if (isNode) {
// TODO: Ideally we should fix this in QUnit instead.
delete window.history;
window.history = {};
QUnit.begin(function() {
if (QUnit.urlParams.hidepassed) {
document.getElementById('qunit-tests').className += ' hidepass';
}
resemble.outputSettings({
errorColor: {
red: 255,
green: 51,
blue: 0
},
errorType: 'flat',
transparency: 1
});
});
}
@ -47,50 +81,50 @@ if (isNode) {
if (!('Base' in root))
paper.install(root);
// Override console.error, so that we can catch errors that are only logged to
// the console.
var errorHandler = console.error;
console.error = function() {
QUnit.pushFailure([].join.call(arguments, ' '), QUnit.config.current.stack);
errorHandler.apply(this, arguments);
};
// NOTE: In order to "export" all methods into the shared Prepro.js scope when
// using node-qunit, we need to define global functions as:
// `var name = function() {}`. `function name() {}` does not work!
QUnit.done(function(details) {
console.error = errorHandler;
});
var currentProject;
var test = function(testName, expected) {
var parameters = expected.toString().match(/^\s*function[^\(]*\(([^\)]*)/)[1];
// If this is running on an older version of QUnit (e.g. node-qunit is stuck
// with v1.10 for now), emulate the new assert.async() syntax through
// QUnit.asyncTest() and QUnit.start();
if (!QUnit.async && parameters === 'assert') {
return QUnit.asyncTest(testName, function() {
// Since tests may be asynchronous, remove the old project before
// running the next test.
if (currentProject)
currentProject.remove();
currentProject = new Project();
// Pass a fake assert object with just the functions that we need,
// so far a async() function returning a done() function:
expected({
var currentProject,
// In case we're stuck with an old QUnit, use a fake assert object with just
// the functions that we need:
// For now, a async() function returning a done() function:
fakeAssert = {
async: function() {
return function() {
QUnit.start();
};
}
});
});
} else {
return QUnit.test(testName, function(assert) {
// Since tests may be asynchronous, remove the old project before
};
// NOTE: In order to "export" all methods into the shared Prepro.js scope when
// using node-qunit, we need to define global functions as:
// `var name = function() {}`. `function name() {}` does not work!
var test = function(testName, expected) {
// If this is running on an older version of QUnit (e.g. node-qunit is stuck
// with v1.10 for now), emulate the new assert.async() syntax through
// QUnit.asyncTest() and QUnit.start();
var emulate = !QUnit.async && 'assert' ===
// Get the parameter list from the passed function to see if we're
// expecting the assert object to do async...
expected.toString().match(/^\s*function[^\(]*\(([^\)]*)/)[1];
return QUnit[emulate ? 'asyncTest' : 'test'](testName, function(assert) {
// Since tests can be asynchronous, remove the old project before
// running the next test.
if (currentProject)
currentProject.remove();
currentProject = new Project();
expected(assert);
expected(emulate ? fakeAssert : assert);
});
}
};
// Override equals to convert functions to message and execute them as tests()
@ -140,33 +174,6 @@ var identicalAfterCloning = {
Symbol: true
};
var getFunctionMessage = function(func) {
var message = func.toString().match(
/^\s*function[^\{]*\{([\s\S]*)\}\s*$/)[1]
.replace(/ /g, '')
.replace(/^\s+|\s+$/g, '');
if (/^return /.test(message)) {
message = message
.replace(/^return /, '')
.replace(/;$/, '');
}
return message;
};
var createSVG = function(str, attrs) {
if (attrs) {
// Similar to SVGExport's createElement / setAttributes.
var node = document.createElementNS('http://www.w3.org/2000/svg', str);
for (var key in attrs)
node.setAttribute(key, attrs[key]);
return node;
} else {
return new window.DOMParser().parseFromString(
'<svg xmlns="http://www.w3.org/2000/svg">' + str + '</svg>',
'text/xml');
}
};
// Register a jsDump parser for Base.
QUnit.jsDump.setParser('Base', function (obj, stack) {
// Just compare the string representation of classes inheriting from Base,
@ -191,8 +198,7 @@ var compareProperties = function(actual, expected, properties, message, options)
}
};
var compareItem = function(actual, expected, message, options, properties) {
options = options || {};
var compareRasterized = function(actual, expected, message, options) {
function rasterize(item, group, resolution) {
var raster = null;
@ -209,7 +215,6 @@ var compareItem = function(actual, expected, message, options, properties) {
+ '" src="' + raster.source + '">';
}
if (options.rasterize) {
// In order to properly compare pixel by pixel, we need to put each item
// into a group with a white background of the united dimensions of the
// bounds of both items before rasterizing.
@ -245,6 +250,14 @@ var compareItem = function(actual, expected, message, options, properties) {
var id = QUnit.config.current.testId,
index = QUnit.config.current.assertions.length + 1,
result;
if (!resemble._setup) {
resemble._setup = true;
resemble.outputSettings({
errorColor: { red: 255, green: 51, blue: 0 },
errorType: 'flat',
transparency: 1
});
}
resemble(actual.getImageData())
.compareTo(expected.getImageData())
// When working with imageData, this call is synchronous:
@ -272,7 +285,13 @@ var compareItem = function(actual, expected, message, options, properties) {
+ '<img src="' + result.getImageDataUrl() + '">';
}
}
} else {
};
var compareItem = function(actual, expected, message, options, properties) {
options = options || {};
if (options.rasterize) {
return compareRasterized(actual, expected, message, options);
}
if (options.cloned)
QUnit.notStrictEqual(actual.id, expected.id,
'not ' + message + '.id');
@ -294,7 +313,6 @@ var compareItem = function(actual, expected, message, options, properties) {
'strokeColor', 'strokeCap', 'strokeJoin', 'dashArray',
'dashOffset', 'miterLimit', 'fontSize', 'font', 'leading',
'justification'], message + '.style', options);
}
};
// A list of comparator functions, based on `expected` type. See equals() for
@ -453,3 +471,33 @@ var comparators = {
message, options);
}
};
// Some other helpers:
var getFunctionMessage = function(func) {
var message = func.toString().match(
/^\s*function[^\{]*\{([\s\S]*)\}\s*$/)[1]
.replace(/ /g, '')
.replace(/^\s+|\s+$/g, '');
if (/^return /.test(message)) {
message = message
.replace(/^return /, '')
.replace(/;$/, '');
}
return message;
};
var createSVG = function(str, attrs) {
if (attrs) {
// Similar to SVGExport's createElement / setAttributes.
var node = document.createElementNS('http://www.w3.org/2000/svg', str);
for (var key in attrs)
node.setAttribute(key, attrs[key]);
return node;
} else {
return new window.DOMParser().parseFromString(
'<svg xmlns="http://www.w3.org/2000/svg">' + str + '</svg>',
'text/xml');
}
};

View file

@ -11,5 +11,7 @@
*/
/*#*/ include('helpers.js');
// We need to load resemble.js after helpers.js, since for Node, helpers makes
// sure window, document and Image are made global first.
/*#*/ include('../node_modules/resemblejs/resemble.js', { namespace: 'resemble' });
/*#*/ include('tests/load.js');

View file

@ -115,8 +115,7 @@ test('Import SVG polyline', function() {
});
test('Import complex CompoundPath and clone', function() {
var svg = createSVG('<path fill="red" d="M4,14h20v-2H4V14z M15,26h7v-2h-7V26z M15,22h9v-2h-9V22z M15,18h9v-2h-9V18z M4,26h9V16H4V26z M28,10V6H0v22c0,0,0,4,4,4 h25c0,0,3-0.062,3-4V10H28z M4,30c-2,0-2-2-2-2V8h24v20c0,0.921,0.284,1.558,0.676,2H4z"/>;');
var item = paper.project.importSVG(svg.firstChild);
var svg = createSVG('<path fill="red" d="M4,14h20v-2H4V14z M15,26h7v-2h-7V26z M15,22h9v-2h-9V22z M15,18h9v-2h-9V18z M4,26h9V16H4V26z M28,10V6H0v22c0,0,0,4,4,4 h25c0,0,3-0.062,3-4V10H28z M4,30c-2,0-2-2-2-2V8h24v20c0,0.921,0.284,1.558,0.676,2H4z"/>');
var item = paper.project.importSVG(svg);
equals(item.clone(), item, null, { cloned: true });
return;
});