From 4c84c3dad54fc939d16e72a981995eca855aefc1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=BCrg=20Lehni?= <juerg@scratchdisk.com>
Date: Wed, 27 Jan 2016 19:57:07 +0100
Subject: [PATCH] Tests: Start getting QUnit tests to work on Node.js
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Work in progress…
---
 .travis.yml                         |   3 +-
 gulp/tasks/test.js                  |  67 ++++++++-
 gulp/utils/error.js                 |   2 +-
 package.json                        |   8 +-
 src/load.js                         |   2 +-
 test/{js => }/helpers.js            | 213 +++++++++++++++++-----------
 test/index.html                     |   9 +-
 test/load.js                        |  15 ++
 test/tests/Color.js                 |   5 +-
 test/tests/CompoundPath.js          |   2 +-
 test/tests/Curve.js                 |   2 +-
 test/tests/CurveLocation.js         |   2 +-
 test/tests/Emitter.js               |   8 +-
 test/tests/Group.js                 |   2 +-
 test/tests/HitResult.js             |   2 +-
 test/tests/Item.js                  |   2 +-
 test/tests/Item_Bounds.js           |   4 +-
 test/tests/Item_Cloning.js          |   2 +
 test/tests/Item_Getting.js          |  21 +--
 test/tests/Item_Order.js            |   2 +
 test/tests/JSON.js                  |   2 +-
 test/tests/Layer.js                 |   2 +-
 test/tests/Matrix.js                |   3 +-
 test/tests/Path.js                  |   2 +-
 test/tests/PathItem_Contains.js     |   2 +-
 test/tests/Path_Boolean.js          |   7 +-
 test/tests/Path_Bounds.js           |   2 +-
 test/tests/Path_Curves.js           |   2 +-
 test/tests/Path_Drawing_Commands.js |   2 +-
 test/tests/Path_Intersections.js    |   2 +-
 test/tests/Path_Length.js           |   2 +-
 test/tests/Path_Shapes.js           |   2 +-
 test/tests/PlacedSymbol.js          |   2 +-
 test/tests/Point.js                 |   5 +-
 test/tests/Project.js               |   2 +-
 test/tests/Raster.js                |  59 +++++---
 test/tests/Rectangle.js             |   2 +-
 test/tests/SVGExport.js             |   2 +-
 test/tests/SVGImport.js             |  11 +-
 test/tests/Segment.js               |   3 +-
 test/tests/Shape.js                 |   2 +-
 test/tests/Size.js                  |   3 +-
 test/tests/Style.js                 |   2 +-
 test/tests/TextItem.js              |   2 +-
 44 files changed, 322 insertions(+), 176 deletions(-)
 rename test/{js => }/helpers.js (79%)
 create mode 100644 test/load.js

diff --git a/.travis.yml b/.travis.yml
index a45680a9..31bc6300 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -18,4 +18,5 @@ addons:
 script:
   - npm run lint
   - gulp minify
-  - gulp test
+  - gulp test:browser
+  - gulp test:node
diff --git a/gulp/tasks/test.js b/gulp/tasks/test.js
index a2ecec31..1c1934ae 100644
--- a/gulp/tasks/test.js
+++ b/gulp/tasks/test.js
@@ -11,9 +11,68 @@
  */
 
 var gulp = require('gulp'),
-    qunit = require('gulp-qunit');
+    gulp_qunit = require('gulp-qunit'),
+    node_qunit = require('qunit'),
+    gutil = require('gulp-util'),
+    extend = require('extend'),
+    minimist = require('minimist');
 
-gulp.task('test', function() {
-    return gulp.src('test/index.html')
-        .pipe(qunit({ timeout: 20, noGlobals: true }));
+// 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
+});
+
+gulp.task('test', ['test:browser']);
+
+gulp.task('test:browser', ['minify:acorn'], function() {
+    return gulp.src('test/index.html')
+        .pipe(gulp_qunit({ timeout: 20, noGlobals: true }));
+});
+
+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');
+    node_qunit.run({
+        maxBlockDuration: 100 * 1000,
+        deps: [
+            // To dynamically load the tests files from the sources, we need to
+            // require Prepro.js first. Since we need a sub-module, we have to
+            // use relative addresses: require('prepro/lib/node') does not work
+            // because of the way node-qunit handles relative addresses.
+            '../node_modules/prepro/lib/node.js',
+            // Note that loading dist/paper-full.js also works in combination
+            // with `gulp load`, in which case Prepro.js is present and handles
+            // the loading transparently.
+            { path: '../dist/paper-full.js', namespace: 'paper' }
+        ],
+        // Now load the actual test files through test/load.js, using Prepro.js
+        // 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);
+    });
 });
diff --git a/gulp/utils/error.js b/gulp/utils/error.js
index 3a404256..c9f0f8a4 100644
--- a/gulp/utils/error.js
+++ b/gulp/utils/error.js
@@ -18,7 +18,7 @@ gulp.on('error', function(err) {
     var msg = err.toString();
     if (msg === '[object Object]')
         msg = err;
-    gutil.log(ERROR, err);
+    gutil.log(ERROR, msg);
     if (err.stack)
         gutil.log(ERROR, err.stack);
     this.emit('end');
diff --git a/package.json b/package.json
index 1a9c3730..03728cf4 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
     "README.md"
   ],
   "engines": {
-    "node": ">=0.8.0 <5.0.0"
+    "node": ">=0.8.0 <6.0.0"
   },
   "dependencies": {
     "jsdom": "git://github.com/lehni/jsdom.git#3d55789d0f4d55392721b1e22890837fde472375",
@@ -42,7 +42,7 @@
     "gulp": "^3.9.0",
     "gulp-cached": "^1.1.0",
     "gulp-jshint": "^2.0.0",
-    "gulp-prepro": "^2.0.0",
+    "gulp-prepro": "^2.1.0",
     "gulp-qunit": "git://github.com/lehni/gulp-qunit.git#459c5603ceac460327a40dc89df6f19c786dc61b",
     "gulp-rename": "^1.2.2",
     "gulp-rimraf": "^0.2.0",
@@ -56,7 +56,9 @@
     "jshint": "2.8.x",
     "jshint-summary": "^0.4.0",
     "merge-stream": "^1.0.0",
-    "prepro": "^2.0.0",
+    "minimist": "^1.2.0",
+    "prepro": "^2.1.0",
+    "qunit": "^0.7.7",
     "qunitjs": "^1.20.0",
     "require-dir": "^0.3.0",
     "resemblejs": "^2.1.0",
diff --git a/src/load.js b/src/load.js
index d5334e4b..89f90da6 100644
--- a/src/load.js
+++ b/src/load.js
@@ -45,7 +45,7 @@ if (typeof window === 'object') {
     }
 } else {
     // Node.js based loading through Prepro.js:
-    var prepro = require('prepro/lib/node.js'),
+    var prepro = require('prepro/lib/node'),
         // Load the default browser-based options for further amendments.
         // Step out and back into src, in case this is loaded from
         // dist/paper-node.js
diff --git a/test/js/helpers.js b/test/helpers.js
similarity index 79%
rename from test/js/helpers.js
rename to test/helpers.js
index 181ecf93..446808be 100644
--- a/test/js/helpers.js
+++ b/test/helpers.js
@@ -10,26 +10,42 @@
  * All rights reserved.
  */
 
-// Until window.history.pushState() works when running locally, we need to trick
-// qunit into thinking that the feature is not present. This appears to work...
-// TODO: Ideally we should fix this in QUnit instead.
-delete window.history;
-window.history = {};
+var isNode = typeof global === 'object',
+    root;
 
-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
+if (isNode) {
+    root = global;
+    // Resemble.js needs the Image constructor this global.
+    global.Image = paper.window.Image;
+} else {
+    root = window;
+    // This is only required when running in the browser:
+    // Until window.history.pushState() works when running locally, we need to
+    // trick qunit into thinking that the feature is not present. This appears
+    // to work...
+    // 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
+        });
     });
-});
+}
+
+// The unit-tests expect the paper classes to be global.
+if (!('Base' in root))
+    paper.install(root);
 
 var errorHandler = console.error;
 console.error = function() {
@@ -37,8 +53,48 @@ console.error = function() {
     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!
+
+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({
+                async: function() {
+                    return function() {
+                        QUnit.start();
+                    };
+                }
+            });
+        });
+    } else {
+        return QUnit.test(testName, function(assert) {
+            // Since tests may be asynchronous, remove the old project before
+            // running the next test.
+            if (currentProject)
+                currentProject.remove();
+            currentProject = new Project();
+            expected(assert);
+        });
+    }
+};
+
 // Override equals to convert functions to message and execute them as tests()
-function equals(actual, expected, message, options) {
+var equals = function(actual, expected, message, options) {
     // Allow the use of functions for actual, which will get called and their
     // source content extracted for readable reports.
     if (typeof actual === 'function') {
@@ -55,7 +111,7 @@ function equals(actual, expected, message, options) {
             || type === 'boolean' && 'Boolean'
             || type === 'undefined' && 'Undefined'
             || Array.isArray(expected) && 'Array'
-            || expected instanceof Element && 'Element' // handle DOM Elements
+            || expected instanceof window.Element && 'Element' // handle DOM Elements
             || (cls = expected && expected._class) // check _class 2nd last
             || type === 'object' && 'Object'; // Object as catch-all
     var comparator = type && comparators[type];
@@ -76,7 +132,40 @@ function equals(actual, expected, message, options) {
                 actual, identical ? expected : 'not ' + expected,
                 message + ': identical after cloning');
     }
-}
+};
+
+// A list of classes that should be identical after their owners were cloned.
+var identicalAfterCloning = {
+    Gradient: true,
+    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) {
@@ -95,14 +184,15 @@ QUnit.jsDump.setParser('object', function (obj, stack) {
             : objectParser).call(this, obj, stack);
 });
 
-function compareProperties(actual, expected, properties, message, options) {
+var compareProperties = function(actual, expected, properties, message, options) {
     for (var i = 0, l = properties.length; i < l; i++) {
         var key = properties[i];
         equals(actual[key], expected[key], message + '.' + key, options);
     }
-}
+};
 
-function compareItem(actual, expected, message, options, properties) {
+var compareItem = function(actual, expected, message, options, properties) {
+    options = options || {};
 
     function rasterize(item, group, resolution) {
         var raster = null;
@@ -119,7 +209,7 @@ function compareItem(actual, expected, message, options, properties) {
                 + '" src="' + raster.source + '">';
     }
 
-    if (options && options.rasterize) {
+    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.
@@ -159,11 +249,15 @@ function compareItem(actual, expected, message, options, properties) {
                 .compareTo(expected.getImageData())
                 // When working with imageData, this call is synchronous:
                 .onComplete(function(data) { result = data; });
-            var identical = result ? 100 - result.misMatchPercentage : 0,
-                ok = identical == 100,
-                text = identical.toFixed(2) + '% identical';
-            QUnit.push(ok, text, '100.00% identical', message);
-            if (!ok && result) {
+            var tolerance = (options.tolerance || 1e-4) * 100, // percentages...
+                fixed = ((1 / tolerance) + '').length - 1,
+                identical = result ? 100 - result.misMatchPercentage : 0,
+                reached = identical.toFixed(fixed),
+                hundred = (100).toFixed(fixed),
+                ok = reached == hundred;
+            QUnit.push(ok, reached + '% identical', hundred + '% identical',
+                    message);
+            if (!ok && result && !isNode) {
                 // Get the right entry for this unit test and assertion, and
                 // replace the results with images
                 var entry = document.getElementById('qunit-test-output-' + id)
@@ -179,14 +273,14 @@ function compareItem(actual, expected, message, options, properties) {
             }
         }
     } else {
-        if (options && options.cloned)
+        if (options.cloned)
             QUnit.notStrictEqual(actual.id, expected.id,
                     'not ' + message + '.id');
         QUnit.strictEqual(actual.constructor, expected.constructor,
                 message + '.constructor');
         // When item is cloned and has a name, the name will be versioned:
         equals(actual.name,
-                options && options.cloned && expected.name
+                options.cloned && expected.name
                     ? expected.name + ' 1' : expected.name,
                 message + '.name');
         compareProperties(actual, expected, ['children', 'bounds', 'position',
@@ -201,7 +295,7 @@ function compareItem(actual, expected, message, options, properties) {
                 'dashOffset', 'miterLimit', 'fontSize', 'font', 'leading',
                 'justification'], message + '.style', options);
     }
-}
+};
 
 // A list of comparator functions, based on `expected` type. See equals() for
 // an explanation of how the type is determined.
@@ -359,56 +453,3 @@ var comparators = {
                 message, options);
     }
 };
-
-// A list of classes that should be identical after their owners were cloned.
-var identicalAfterCloning = {
-    Gradient: true,
-    Symbol: true
-};
-
-function getFunctionMessage(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;
-}
-
-function test(testName, expected) {
-    return QUnit.test(testName, function() {
-        var project = new Project();
-        expected();
-        project.remove();
-    });
-}
-
-function asyncTest(testName, expected) {
-    return QUnit.asyncTest(testName, function() {
-        var project = new Project();
-        expected(function() {
-            project.remove();
-            QUnit.start();
-        });
-    });
-}
-
-// SVG
-
-function createSVG(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');
-    }
-}
diff --git a/test/index.html b/test/index.html
index c5a88307..3fc6d7b1 100644
--- a/test/index.html
+++ b/test/index.html
@@ -3,11 +3,10 @@
 <head>
     <title>Paper.js Tests</title>
     <link rel="stylesheet" href="../node_modules/qunitjs/qunit/qunit.css">
-    <script type="text/javascript" src="../node_modules/qunitjs/qunit/qunit.js"></script>
-    <script type="text/javascript" src="../node_modules/resemblejs/resemble.js"></script>
-    <script type="text/javascript" src="js/helpers.js"></script>
-    <script type="text/javascript" src="../src/load.js"></script>
-    <script type="text/javascript" src="tests/load.js"></script>
+    <script src="../node_modules/qunitjs/qunit/qunit.js"></script>
+    <script src="../dist/paper-full.js"></script>
+    <script src="../node_modules/prepro/lib/browser.js"></script>
+    <script src="load.js"></script>
 </head>
 <body>
     <h1 id="qunit-header">QUnit Test Suite</h1>
diff --git a/test/load.js b/test/load.js
new file mode 100644
index 00000000..2a1f3379
--- /dev/null
+++ b/test/load.js
@@ -0,0 +1,15 @@
+/*
+ * Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
+ * http://paperjs.org/
+ *
+ * Copyright (c) 2011 - 2016, Juerg Lehni & Jonathan Puckey
+ * http://scratchdisk.com/ & http://jonathanpuckey.com/
+ *
+ * Distributed under the MIT license. See LICENSE file for details.
+ *
+ * All rights reserved.
+ */
+
+/*#*/ include('helpers.js');
+/*#*/ include('../node_modules/resemblejs/resemble.js', { namespace: 'resemble' });
+/*#*/ include('tests/load.js');
diff --git a/test/tests/Color.js b/test/tests/Color.js
index 3467d23a..5467399d 100644
--- a/test/tests/Color.js
+++ b/test/tests/Color.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Color');
+QUnit.module('Color');
 
 test('Set named color', function() {
     var path = new Path();
@@ -222,6 +222,3 @@ test('Color#divide', function() {
     var color = new Color(1, 1, 1);
     equals(color.divide(4), new Color([0.25, 0.25, 0.25]));
 });
-
-
-
diff --git a/test/tests/CompoundPath.js b/test/tests/CompoundPath.js
index fb2c346d..78cc876a 100644
--- a/test/tests/CompoundPath.js
+++ b/test/tests/CompoundPath.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Compound Path');
+QUnit.module('Compound Path');
 
 test('moveTo / lineTo', function() {
     var path = new CompoundPath();
diff --git a/test/tests/Curve.js b/test/tests/Curve.js
index f9cd433c..48b16e5b 100644
--- a/test/tests/Curve.js
+++ b/test/tests/Curve.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Curve');
+QUnit.module('Curve');
 
 test('Curve#getParameterOf()', function() {
     // For issue #708:
diff --git a/test/tests/CurveLocation.js b/test/tests/CurveLocation.js
index d1dc201a..fa3b06c2 100644
--- a/test/tests/CurveLocation.js
+++ b/test/tests/CurveLocation.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('CurveLocation');
+QUnit.module('CurveLocation');
 
 test('CurveLocation#offset', function() {
     var path = new Path();
diff --git a/test/tests/Emitter.js b/test/tests/Emitter.js
index ec745a2c..cef93fb9 100644
--- a/test/tests/Emitter.js
+++ b/test/tests/Emitter.js
@@ -10,10 +10,10 @@
  * All rights reserved.
  */
 
-module('Emitter');
+QUnit.module('Emitter');
 
 test('on()', function() {
-    var emitter = new Base(Emitter),
+    var emitter = new Item(),
         installed;
     // fake event type registration
     emitter._eventTypes = {mousemove: {install: function(){ installed = true;} } };
@@ -37,7 +37,7 @@ test('on()', function() {
 });
 
 test('off()', function() {
-    var emitter = new Base(Emitter),
+    var emitter = new Item(),
         uninstalled, called = 0,
         handler = function () {called++},
         handler2 = function () {};
@@ -68,7 +68,7 @@ test('off()', function() {
 });
 
 test('emit()', function() {
-    var emitter = new Base(Emitter),
+    var emitter = new Item(),
         called,
         handler = function (e) {called = e};
     // fake event type registration
diff --git a/test/tests/Group.js b/test/tests/Group.js
index 2c208e28..fdc72e6a 100644
--- a/test/tests/Group.js
+++ b/test/tests/Group.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Group');
+QUnit.module('Group');
 
 test('new Group()', function() {
     var group = new Group();
diff --git a/test/tests/HitResult.js b/test/tests/HitResult.js
index d692e180..52f5d34f 100644
--- a/test/tests/HitResult.js
+++ b/test/tests/HitResult.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('HitResult');
+QUnit.module('HitResult');
 
 test('Hit-testing options', function() {
     var defaultOptions = {
diff --git a/test/tests/Item.js b/test/tests/Item.js
index 25485509..3e52abaf 100644
--- a/test/tests/Item.js
+++ b/test/tests/Item.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Item');
+QUnit.module('Item');
 
 test('copyTo(project)', function() {
     var project = paper.project;
diff --git a/test/tests/Item_Bounds.js b/test/tests/Item_Bounds.js
index 0991383c..8e375544 100644
--- a/test/tests/Item_Bounds.js
+++ b/test/tests/Item_Bounds.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Item Bounds');
+QUnit.module('Item Bounds');
 
 test('item.bounds caching', function() {
     var circle = new Path.Circle(new Point(100, 100), 50);
@@ -94,5 +94,5 @@ test('text.bounds', function() {
     var text = new PointText(new Point(50, 100));
     text.fillColor = 'black';
     text.content = 'This is a test';
-    equals(text.bounds, new Rectangle(50, 89.2, 67, 14.4), 'text.bounds', { tolerance: 0.5 });
+    equals(text.bounds, new Rectangle(50, 89.2, 67, 14.4), 'text.bounds', { tolerance: 1 });
 });
diff --git a/test/tests/Item_Cloning.js b/test/tests/Item_Cloning.js
index 9afdd094..c9009546 100644
--- a/test/tests/Item_Cloning.js
+++ b/test/tests/Item_Cloning.js
@@ -10,6 +10,8 @@
  * All rights reserved.
  */
 
+QUnit.module('Item Cloning');
+
 function cloneAndCompare(item) {
     var copy = item.clone();
     equals(function() {
diff --git a/test/tests/Item_Getting.js b/test/tests/Item_Getting.js
index 7cc2efa5..ec8c8912 100644
--- a/test/tests/Item_Getting.js
+++ b/test/tests/Item_Getting.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Getting and Matching Items');
+QUnit.module('Getting and Matching Items');
 
 test('Item#getItems()', function() {
     var group = new Group([new Path({ selected: true }), new Raster()]);
@@ -72,14 +72,14 @@ test('Project#getItems()', function() {
         className: 'Group'
     });
     equals(function() {
-        return matches.length == 1 && matches[0] === group
+        return matches.length == 1 && matches[0] === group;
     }, true);
 
     var matches = paper.project.getItems({
         type: 'group'
     });
     equals(function() {
-        return matches.length == 1 && matches[0] === group
+        return matches.length == 1 && matches[0] === group;
     }, true);
 
     var raster = new Raster();
@@ -87,7 +87,7 @@ test('Project#getItems()', function() {
         class: Raster
     });
     equals(function() {
-        return matches.length == 1 && matches[0] === raster
+        return matches.length == 1 && matches[0] === raster;
     }, true);
 
     equals(function() {
@@ -120,7 +120,7 @@ test('Project#getItems() with compare function', function() {
 
     var items = paper.project.getItems({
         opacity: function(value) {
-            return value < 1
+            return value < 1;
         }
     });
     equals(function() {
@@ -162,23 +162,26 @@ test('Project#getItems() with color', function() {
 });
 
 test('Project#getItems() with regex function', function() {
-    var decoyPath = new Path({
+    var layer = paper.project.activeLayer;
+    var stopPath = new Path({
         name: 'stop'
     });
 
-    var decoyPath2 = new Path({
+    var pausePath = new Path({
         name: 'pause'
     });
 
-    var path = new Path({
+    var startPath = new Path({
         name: 'starting'
     });
 
     var items = paper.project.getItems({
         name: /^start/g
     });
+
+    // console.log(paper.project.activeLayer);
     equals(function() {
-        return items.length == 1 && items[0] == path;
+        return items.length == 1 && items[0] == startPath;
     }, true);
 
     equals(function() {
diff --git a/test/tests/Item_Order.js b/test/tests/Item_Order.js
index af3b2f83..94b06eed 100644
--- a/test/tests/Item_Order.js
+++ b/test/tests/Item_Order.js
@@ -10,6 +10,8 @@
  * All rights reserved.
  */
 
+QUnit.module('Item Order');
+
 test('Item Order', function() {
     var line = new Path();
     line.add([0, 0], [100, 100]);
diff --git a/test/tests/JSON.js b/test/tests/JSON.js
index d41e9e39..bb93faba 100644
--- a/test/tests/JSON.js
+++ b/test/tests/JSON.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('JSON');
+QUnit.module('JSON');
 
 function testExportImportJSON(project) {
     // Use higher precision than in comparissons, for bounds
diff --git a/test/tests/Layer.js b/test/tests/Layer.js
index 6b89b4e8..ff1330b5 100644
--- a/test/tests/Layer.js
+++ b/test/tests/Layer.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Layer');
+QUnit.module('Layer');
 
 test('previousSibling / nextSibling', function() {
     var project = paper.project;
diff --git a/test/tests/Matrix.js b/test/tests/Matrix.js
index 90c61253..9b5871be 100644
--- a/test/tests/Matrix.js
+++ b/test/tests/Matrix.js
@@ -10,7 +10,8 @@
  * All rights reserved.
  */
 
-module('Matrix');
+QUnit.module('Matrix');
+
 test('Decomposition: rotate()', function() {
     function testAngle(a, ea) {
         var m = new Matrix().rotate(a),
diff --git a/test/tests/Path.js b/test/tests/Path.js
index 4e80ea59..b6a034bd 100644
--- a/test/tests/Path.js
+++ b/test/tests/Path.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Path');
+QUnit.module('Path');
 
 test('path.join(path)', function() {
     var path = new Path();
diff --git a/test/tests/PathItem_Contains.js b/test/tests/PathItem_Contains.js
index deae04a9..cc9e9de6 100644
--- a/test/tests/PathItem_Contains.js
+++ b/test/tests/PathItem_Contains.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('PathItem Contains');
+QUnit.module('PathItem Contains');
 
 function testPoint(item, point, inside, message) {
     equals(item.contains(point), inside, message || ('The point ' + point
diff --git a/test/tests/Path_Boolean.js b/test/tests/Path_Boolean.js
index 384e4637..ac40c21c 100644
--- a/test/tests/Path_Boolean.js
+++ b/test/tests/Path_Boolean.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Path Boolean Operations');
+QUnit.module('Path Boolean Operations');
 
 function createPath(str) {
     var ctor = (str.match(/z/gi) || []).length > 1 ? CompoundPath : Path;
@@ -30,7 +30,7 @@ function compareBoolean(actual, expected, message, options) {
         strokeColor: 'black',
         fillColor: expected.closed ? 'yellow' : null
     };
-    equals(actual, expected, message, options || { rasterize: true });
+    equals(actual, expected, message, Base.set({ rasterize: true }, options));
 }
 
 test('#541', function() {
@@ -884,7 +884,8 @@ test('Isolated edge-cases from @iconexperience\'s boolean-test suite', function(
         closed: true
     });
     compareBoolean(function() { return path1.unite(); },
-        'M428.65987,123.24313c0,0 18.24445,159.97772 20.21157,166.76806c-3.05664,-6.18082 -73.53131,-139.25432 -73.53131,-139.25432z M448.97323,290.23336c0,0 0,0 0,0c0.22704,0.04317 -0.06896,-0.00471 0,0c-0.02659,-0.00506 -0.06063,-0.08007 -0.1018,-0.22217c0.07286,0.14733 0.10741,0.22256 0.1018,0.22217z');
+        'M428.65987,123.24313c0,0 18.24445,159.97772 20.21157,166.76806c-3.05664,-6.18082 -73.53131,-139.25432 -73.53131,-139.25432z M448.97323,290.23336c0,0 0,0 0,0c0.22704,0.04317 -0.06896,-0.00471 0,0c-0.02659,-0.00506 -0.06063,-0.08007 -0.1018,-0.22217c0.07286,0.14733 0.10741,0.22256 0.1018,0.22217z',
+        null, { tolerance: 1e-3 });
 
     // #784#issuecomment-168605018
     var path1 = new CompoundPath();
diff --git a/test/tests/Path_Bounds.js b/test/tests/Path_Bounds.js
index b2456387..6a51e9f2 100644
--- a/test/tests/Path_Bounds.js
+++ b/test/tests/Path_Bounds.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Path Bounds');
+QUnit.module('Path Bounds');
 
 test('path.bounds', function() {
     var path = new Path([
diff --git a/test/tests/Path_Curves.js b/test/tests/Path_Curves.js
index ec10ff8a..2ead8045 100644
--- a/test/tests/Path_Curves.js
+++ b/test/tests/Path_Curves.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Path Curves');
+QUnit.module('Path Curves');
 
 test('path.curves synchronisation', function() {
     var path = new Path();
diff --git a/test/tests/Path_Drawing_Commands.js b/test/tests/Path_Drawing_Commands.js
index f73a9e60..af1c68300 100644
--- a/test/tests/Path_Drawing_Commands.js
+++ b/test/tests/Path_Drawing_Commands.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Path Drawing Commands');
+QUnit.module('Path Drawing Commands');
 
 test('path.lineTo(point);', function() {
     var path = new Path();
diff --git a/test/tests/Path_Intersections.js b/test/tests/Path_Intersections.js
index 99f963e3..2d3b7a0d 100644
--- a/test/tests/Path_Intersections.js
+++ b/test/tests/Path_Intersections.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Path Intersections');
+QUnit.module('Path Intersections');
 
 function testIntersection(intersections, results) {
     equals(intersections.length, results.length, 'intersections.length');
diff --git a/test/tests/Path_Length.js b/test/tests/Path_Length.js
index 26a2b663..fb77b4f9 100644
--- a/test/tests/Path_Length.js
+++ b/test/tests/Path_Length.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Path Length');
+QUnit.module('Path Length');
 
 test('path.length', function() {
     var path = new Path([
diff --git a/test/tests/Path_Shapes.js b/test/tests/Path_Shapes.js
index c56b2930..bcd9abde 100644
--- a/test/tests/Path_Shapes.js
+++ b/test/tests/Path_Shapes.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Predefined Path Shapes');
+QUnit.module('Predefined Path Shapes');
 
 test('new Path.Rectangle([50, 50], [100, 100])', function() {
     var path = new Path.Rectangle([50, 50], [100, 100]);
diff --git a/test/tests/PlacedSymbol.js b/test/tests/PlacedSymbol.js
index 1aff4fd6..466b7523 100644
--- a/test/tests/PlacedSymbol.js
+++ b/test/tests/PlacedSymbol.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Symbol & Placed Symbol');
+QUnit.module('Symbol & Placed Symbol');
 
 test('placedSymbol bounds', function() {
     var path = new Path.Circle([50, 50], 50);
diff --git a/test/tests/Point.js b/test/tests/Point.js
index befd61f9..71a4e94d 100644
--- a/test/tests/Point.js
+++ b/test/tests/Point.js
@@ -10,7 +10,8 @@
  * All rights reserved.
  */
 
-module('Point');
+QUnit.module('Point');
+
 test('new Point(10, 20)', function() {
     var point = new Point(10, 20);
     equals(point.x, 10, 'point.x');
@@ -44,8 +45,6 @@ test('new Point({ angle: 45, length: 20})', function() {
     equals(point, new Point(15.32089, 12.85575));
 });
 
-module('Point vector operations');
-
 test('normalize(length)', function() {
     var point = new Point(0, 10).normalize(20);
     equals(point, new Point(0, 20));
diff --git a/test/tests/Project.js b/test/tests/Project.js
index ef842e4d..ca970d01 100644
--- a/test/tests/Project.js
+++ b/test/tests/Project.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Project');
+QUnit.module('Project');
 
 test('activate()', function() {
     var project = new Project();
diff --git a/test/tests/Raster.js b/test/tests/Raster.js
index 5cdeaa71..05c23d0b 100644
--- a/test/tests/Raster.js
+++ b/test/tests/Raster.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Raster');
+QUnit.module('Raster');
 
 test('Create a raster without a source and check its size', function() {
     var raster = new Raster();
@@ -23,23 +23,34 @@ test('Create a raster without a source and set its size', function() {
     equals(raster.size, new Size(640, 480), true);
 });
 
-asyncTest('Create a raster from a url', function(callback) {
+test('Create a raster from a url', function(assert) {
+    var done = assert.async();
     var raster = new Raster('assets/paper-js.gif');
     raster.onLoad = function() {
         equals(raster.size, new Size(146, 146), true);
-        callback();
+        done();
+    };
+    raster.onError = function(event) {
+        pushFailure(event.event);
+        done();
     };
 });
 
-asyncTest('Create a raster from a data url', function(callback) {
+test('Create a raster from a data url', function(assert) {
+    var done = assert.async();
     var raster = new Raster('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABlJREFUeNpi+s/AwPCfgYmR4f9/hv8AAQYAHiAFAS8Lwy8AAAAASUVORK5CYII=');
     raster.onLoad = function() {
         equals(raster.size, new Size(2, 2), true);
-        callback();
+        done();
+    };
+    raster.onError = function(event) {
+        pushFailure(event.event);
+        done();
     };
 });
 
-asyncTest('Create a raster from a dom image', function(callback) {
+test('Create a raster from a dom image', function(assert) {
+    var done = assert.async();
     var img = document.createElement('img');
     img.src = 'assets/paper-js.gif';
     document.body.appendChild(img);
@@ -48,19 +59,19 @@ asyncTest('Create a raster from a dom image', function(callback) {
             var raster = new Raster(img);
             equals(raster.size, new Size(146, 146), true);
             document.body.removeChild(img);
-            callback();
+            done();
         }
     });
 });
 
-test('Create a raster from a canvas', function(callback) {
-    var canvas = CanvasProvider.getCanvas(30, 20);
+test('Create a raster from a canvas', function() {
+    var canvas = paper.createCanvas(30, 20);
     var raster = new Raster(canvas);
     equals(raster.size, new Size(30, 20), true);
-    CanvasProvider.release(canvas);
 });
 
-asyncTest('Create a raster from a dom id', function(callback) {
+test('Create a raster from a dom id', function(assert) {
+    var done = assert.async();
     var img = document.createElement('img');
     img.src = 'assets/paper-js.gif';
     img.id = 'testimage';
@@ -70,12 +81,13 @@ asyncTest('Create a raster from a dom id', function(callback) {
             var raster = new Raster('testimage');
             equals(raster.size, new Size(146, 146), true);
             document.body.removeChild(img);
-            callback();
+            done();
         }
     });
 });
 
-asyncTest('Raster#getPixel / setPixel', function(callback) {
+test('Raster#getPixel / setPixel', function(assert) {
+    var done = assert.async();
     var raster = new Raster('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABlJREFUeNpi+s/AwPCfgYmR4f9/hv8AAQYAHiAFAS8Lwy8AAAAASUVORK5CYII=');
     raster.onLoad = function() {
         equals(raster.getPixel(0, 0), new Color(1, 0, 0, 1));
@@ -86,12 +98,17 @@ asyncTest('Raster#getPixel / setPixel', function(callback) {
         // Alpha
         var color = new Color(1, 1, 0, 0.50196);
         raster.setPixel([0, 0], color);
-        equals(raster.getPixel([0, 0]), color, 'alpha');
-        callback();
+        equals(raster.getPixel([0, 0]), color, 'alpha', { tolerance: 1e-2 });
+        done();
+    };
+    raster.onError = function(event) {
+        pushFailure(event.event);
+        done();
     };
 });
 
-asyncTest('Raster#getSubCanvas', function(callback) {
+test('Raster#getSubCanvas', function(assert) {
+    var done = assert.async();
     var raster = new Raster('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABlJREFUeNpi+s/AwPCfgYmR4f9/hv8AAQYAHiAFAS8Lwy8AAAAASUVORK5CYII=');
     raster.onLoad = function() {
         var canvas = raster.getSubCanvas(new Rectangle({
@@ -114,7 +131,11 @@ asyncTest('Raster#getSubCanvas', function(callback) {
         equals(function() {
             return Base.equals(Array.prototype.slice.call(ctx.getImageData(0, 0, 1, 2).data), expected);
         }, true);
-        callback();
+        done();
+    };
+    raster.onError = function(event) {
+        pushFailure(event.event);
+        done();
     };
 });
 
@@ -132,7 +153,7 @@ test('Raster#getAverageColor(path)', function() {
     var raster = paper.project.activeLayer.rasterize(72);
     circle.scale(0.8);
     equals(raster.getAverageColor(circle), circle.fillColor, null,
-            { tolerance: 10e-4 });
+            { tolerance: 1e-3 });
 });
 
 test('Raster#getAverageColor(path) with compound path', function() {
@@ -155,5 +176,5 @@ test('Raster#getAverageColor(path) with compound path', function() {
     path.scale(0.8);
     path2.scale(1.2);
     equals(raster.getAverageColor(compoundPath), new Color(1, 0, 0), null,
-            { tolerance: 10e-4 });
+            { tolerance: 1e-3 });
 });
diff --git a/test/tests/Rectangle.js b/test/tests/Rectangle.js
index 933c8033..fc7e19a5 100644
--- a/test/tests/Rectangle.js
+++ b/test/tests/Rectangle.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Rectangle');
+QUnit.module('Rectangle');
 
 test('new Rectangle(new Point(10, 20), new Size(30, 40));', function() {
     var rect = new Rectangle(new Point(10, 20), new Size(30, 40));
diff --git a/test/tests/SVGExport.js b/test/tests/SVGExport.js
index 212e6c39..7de9a373 100644
--- a/test/tests/SVGExport.js
+++ b/test/tests/SVGExport.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('SVGExport');
+QUnit.module('SVGExport');
 
 test('Export SVG line', function() {
     var attrs = {
diff --git a/test/tests/SVGImport.js b/test/tests/SVGImport.js
index 0f8ea5b3..90f6a5d7 100644
--- a/test/tests/SVGImport.js
+++ b/test/tests/SVGImport.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('SVGImport');
+QUnit.module('SVGImport');
 
 test('Import SVG line', function() {
     var attrs = {
@@ -59,7 +59,7 @@ test('Import SVG ellipse', function() {
         cy: 80,
         rx: 100,
         ry: 50
-    }
+    };
     var imported = paper.project.importSVG(createSVG('ellipse', attrs),
             { expandShapes: true });
     var path = new Path.Ellipse({
@@ -74,7 +74,7 @@ test('Import SVG circle', function() {
         cx: 100,
         cy: 80,
         r: 50
-    }
+    };
     var imported = paper.project.importSVG(createSVG('circle', attrs),
             { expandShapes: true });
     var path = new Path.Circle({
@@ -115,7 +115,8 @@ test('Import SVG polyline', function() {
 });
 
 test('Import complex CompoundPath and clone', function() {
-    var svg = createSVG('<path id="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.getElementById('path'));
+    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);
     equals(item.clone(), item, null, { cloned: true });
+    return;
 });
diff --git a/test/tests/Segment.js b/test/tests/Segment.js
index 4877d403..24ce21e9 100644
--- a/test/tests/Segment.js
+++ b/test/tests/Segment.js
@@ -10,7 +10,8 @@
  * All rights reserved.
  */
 
-module('Segment');
+QUnit.module('Segment');
+
 test('new Segment(point)', function() {
     var segment = new Segment(new Point(10, 10));
     equals(segment.toString(), '{ point: { x: 10, y: 10 } }');
diff --git a/test/tests/Shape.js b/test/tests/Shape.js
index 6ed45ec0..cc5b59fd 100644
--- a/test/tests/Shape.js
+++ b/test/tests/Shape.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Shape');
+QUnit.module('Shape');
 
 test('shape.toPath().toShape()', function() {
     var shapes = {
diff --git a/test/tests/Size.js b/test/tests/Size.js
index 52f8be18..bb953a7d 100644
--- a/test/tests/Size.js
+++ b/test/tests/Size.js
@@ -10,7 +10,8 @@
  * All rights reserved.
  */
 
-module('Size');
+QUnit.module('Size');
+
 test('new Size(10, 20)', function() {
     var size = new Size(10, 20);
     equals(size.toString(), '{ width: 10, height: 20 }');
diff --git a/test/tests/Style.js b/test/tests/Style.js
index 779e94c0..d3d53002 100644
--- a/test/tests/Style.js
+++ b/test/tests/Style.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('Style');
+QUnit.module('Style');
 
 test('style defaults', function() {
     var path = new Path();
diff --git a/test/tests/TextItem.js b/test/tests/TextItem.js
index 1c76a4d1..69b4f876 100644
--- a/test/tests/TextItem.js
+++ b/test/tests/TextItem.js
@@ -10,7 +10,7 @@
  * All rights reserved.
  */
 
-module('TextItem');
+QUnit.module('TextItem');
 
 test('PointText', function() {
     var text = new PointText({