mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2024-12-28 00:42:54 -05:00
699 lines
25 KiB
JavaScript
699 lines
25 KiB
JavaScript
/*
|
|
* Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
|
|
* http://paperjs.org/
|
|
*
|
|
* Copyright (c) 2011 - 2020, Jürg Lehni & Jonathan Puckey
|
|
* http://juerglehni.com/ & https://puckey.studio/
|
|
*
|
|
* Distributed under the MIT license. See LICENSE file for details.
|
|
*
|
|
* All rights reserved.
|
|
*/
|
|
|
|
// We call our variable `isNodeContext` because resemble.js exposes a global
|
|
// `isNode` function which would override it and break node check.
|
|
var isNodeContext = typeof global === 'object',
|
|
isPhantomContext = typeof window === 'object' && !!window.callPhantom,
|
|
scope;
|
|
|
|
if (isNodeContext) {
|
|
scope = global;
|
|
// Resemble.js needs the Image constructor global.
|
|
global.Image = paper.window.Image;
|
|
} else {
|
|
scope = 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';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Some native javascript classes have name collisions with Paper.js classes.
|
|
// If they have not already been stored in `src/load.js`, do it now:
|
|
var nativeClasses = this.nativeClasses || {
|
|
Event: this.Event || {},
|
|
MouseEvent: this.MouseEvent || {}
|
|
};
|
|
|
|
// The unit-tests expect the paper classes to be global.
|
|
paper.install(scope);
|
|
|
|
// Override console.error, so that we can catch errors that are only logged to
|
|
// the console.
|
|
var errorHandler = console.error;
|
|
console.error = function() {
|
|
var current = QUnit.config.current;
|
|
if (current) {
|
|
QUnit.pushFailure([].join.call(arguments, ' '), current.stack);
|
|
}
|
|
errorHandler.apply(this, arguments);
|
|
};
|
|
|
|
var currentProject;
|
|
|
|
QUnit.done(function(details) {
|
|
console.error = errorHandler;
|
|
// Clear all event listeners after final test.
|
|
if (currentProject) {
|
|
currentProject.remove();
|
|
}
|
|
});
|
|
|
|
// 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) {
|
|
return QUnit.test(testName, function(assert) {
|
|
// Since tests can be asynchronous, remove the old project before
|
|
// running the next test.
|
|
if (currentProject) {
|
|
currentProject.remove();
|
|
// This is needed for interactions tests, to make sure that test is
|
|
// run with a fresh state.
|
|
View._resetState();
|
|
}
|
|
|
|
// Instantiate project with 100x100 pixels canvas instead of default
|
|
// 1x1 to make interactions tests simpler by working with integers.
|
|
currentProject = new Project(new Size(100, 100));
|
|
expected(assert);
|
|
});
|
|
};
|
|
|
|
// Override equals to convert functions to message and execute them as tests()
|
|
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') {
|
|
if (!message)
|
|
message = getFunctionMessage(actual);
|
|
actual = actual();
|
|
}
|
|
// Get the comparator based on the expected value's type only and ignore the
|
|
// actual value's type.
|
|
var type = typeof expected,
|
|
cls;
|
|
type = expected === null && 'Null'
|
|
|| type === 'number' && 'Number'
|
|
|| type === 'boolean' && 'Boolean'
|
|
|| type === 'undefined' && 'Undefined'
|
|
|| Array.isArray(expected) && 'Array'
|
|
|| expected instanceof window.Element && 'Element' // DOM Elements
|
|
|| (cls = expected && expected._class) // check _class 2nd last
|
|
|| type === 'object' && 'Object'; // Object as catch-all
|
|
var comparator = type && comparators[type];
|
|
if (!message)
|
|
message = type ? type.toLowerCase() : 'value';
|
|
if (comparator) {
|
|
comparator(actual, expected, message, options);
|
|
} else if (expected && expected.equals) {
|
|
// Fall back to equals
|
|
QUnit.push(expected.equals(actual), actual, expected, message);
|
|
} else {
|
|
// Finally perform a strict compare
|
|
QUnit.push(actual === expected, actual, expected, message);
|
|
}
|
|
if (options && options.cloned && cls) {
|
|
var identical = identicalAfterCloning[cls];
|
|
QUnit.push(identical ? actual === expected : actual !== expected,
|
|
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,
|
|
SymbolDefinition: true
|
|
};
|
|
|
|
// Register a jsDump parser for Base.
|
|
QUnit.jsDump.setParser('Base', function (obj, stack) {
|
|
// Just compare the string representation of classes inheriting from Base,
|
|
// since they hide the internal values.
|
|
return obj.toString();
|
|
});
|
|
|
|
// Override the default object parser to handle Base objects.
|
|
// We need to keep a reference to the previous implementation.
|
|
var objectParser = QUnit.jsDump.parsers.object;
|
|
|
|
QUnit.jsDump.setParser('object', function (obj, stack) {
|
|
return (obj instanceof Base
|
|
? QUnit.jsDump.parsers.Base
|
|
: objectParser).call(this, obj, stack);
|
|
});
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Compare 2 image data with resemble.js library.
|
|
* When comparison fails, expected, actual and compared images are displayed.
|
|
* @param {ImageData} imageData1 the expected image data
|
|
* @param {ImageData} imageData2 the actual image data
|
|
* @param {number} tolerance
|
|
* @param {string} diffDetail text displayed when comparison fails
|
|
*/
|
|
var compareImageData = function(imageData1, imageData2, tolerance, diffDetail) {
|
|
/**
|
|
* Build an image element from a given image data.
|
|
* @param {ImageData} imageData
|
|
* @return {HTMLImageElement}
|
|
*/
|
|
function image(imageData) {
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = imageData.width;
|
|
canvas.height = imageData.height;
|
|
canvas.getContext('2d').putImageData(imageData, 0, 0);
|
|
var image = new Image();
|
|
image.src = canvas.toDataURL();
|
|
canvas.remove();
|
|
return image;
|
|
}
|
|
|
|
tolerance = (tolerance || 1e-4) * 100;
|
|
|
|
var id = QUnit.config.current.testId,
|
|
index = QUnit.config.current.assertions.length + 1,
|
|
result;
|
|
// Compare image-data using resemble.js:
|
|
resemble.compare(
|
|
imageData1,
|
|
imageData2,
|
|
{
|
|
output: {
|
|
errorColor: { red: 255, green: 51, blue: 0 },
|
|
errorType: 'flat',
|
|
transparency: 1
|
|
},
|
|
ignore: ['antialiasing']
|
|
},
|
|
// When working with imageData, this call is synchronous:
|
|
function (error, data) {
|
|
if (error) {
|
|
console.error(error);
|
|
} else {
|
|
result = data;
|
|
}
|
|
}
|
|
)
|
|
// Compare with tolerance in percentage...
|
|
var fixed = tolerance < 1 ? ((1 / tolerance) + '').length - 1 : 0,
|
|
identical = result ? 100 - result.misMatchPercentage : 0,
|
|
ok = Math.abs(100 - identical) <= tolerance,
|
|
text = identical.toFixed(fixed) + '% identical',
|
|
detail = text;
|
|
if (!ok && diffDetail) {
|
|
detail += diffDetail;
|
|
}
|
|
QUnit.push(ok, text, (100).toFixed(fixed) + '% identical');
|
|
if (!ok && result && !isNodeContext) {
|
|
// Get the right entry for this unit test and assertion, and
|
|
// replace the results with images
|
|
var entry = document.getElementById('qunit-test-output-' + id)
|
|
.querySelector('li:nth-child(' + (index) + ')'),
|
|
bounds = result.diffBounds;
|
|
entry.querySelector('.test-expected td').appendChild(image(imageData2));
|
|
entry.querySelector('.test-actual td').appendChild(image(imageData1));
|
|
entry.querySelector('.test-diff td').innerHTML = '<pre>' + detail
|
|
+ '</pre><br>'
|
|
+ '<img src="' + result.getImageDataUrl() + '">';
|
|
}
|
|
};
|
|
|
|
var comparePixels = function(actual, expected, message, options) {
|
|
function rasterize(item, group, resolution) {
|
|
var raster = null;
|
|
if (group) {
|
|
var parent = item.parent,
|
|
index = item.index;
|
|
group.addChild(item);
|
|
raster = group.rasterize(resolution, false);
|
|
if (parent) {
|
|
parent.insertChild(index, item);
|
|
} else {
|
|
item.remove();
|
|
}
|
|
}
|
|
return raster;
|
|
}
|
|
|
|
if (!expected) {
|
|
return QUnit.strictEqual(actual, expected, message, options);
|
|
} else if (!actual) {
|
|
// In order to compare pixels, just create an empty item that can be
|
|
// rasterized to an empty raster.
|
|
actual = new Group();
|
|
}
|
|
|
|
options = options || {};
|
|
// 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.
|
|
var resolution = options.resolution || 72,
|
|
actualBounds = actual.strokeBounds,
|
|
expectedBounds = expected.strokeBounds,
|
|
bounds = actualBounds.isEmpty()
|
|
? expectedBounds
|
|
: expectedBounds.isEmpty()
|
|
? actualBounds
|
|
: actualBounds.unite(expectedBounds);
|
|
if (bounds.isEmpty()) {
|
|
QUnit.equal('empty', 'empty', message);
|
|
return;
|
|
}
|
|
var group = actual && expected && new Group({
|
|
insert: false,
|
|
children: [
|
|
new Shape.Rectangle({
|
|
rectangle: bounds,
|
|
fillColor: 'white'
|
|
})
|
|
]
|
|
}),
|
|
actualRaster = rasterize(actual, group, resolution),
|
|
expectedRaster = rasterize(expected, group, resolution);
|
|
if (!actualRaster || !expectedRaster) {
|
|
QUnit.push(false, null, null, 'Unable to compare rasterized items: ' +
|
|
(!actualRaster ? 'actual' : 'expected') + ' item is null',
|
|
QUnit.stack(2));
|
|
} else {
|
|
// Compare the two rasterized items.
|
|
var detail = actual instanceof PathItem && expected instanceof PathItem
|
|
? '\nExpected:\n' + expected.pathData +
|
|
'\nActual:\n' + actual.pathData
|
|
: '';
|
|
compareImageData(
|
|
actualRaster.getImageData(),
|
|
expectedRaster.getImageData(),
|
|
options.tolerance,
|
|
detail
|
|
);
|
|
}
|
|
};
|
|
|
|
var compareItem = function(actual, expected, message, options, properties) {
|
|
options = options || {};
|
|
if (options.rasterize) {
|
|
comparePixels(actual, expected, message, options);
|
|
} else if (!actual || !expected) {
|
|
QUnit.strictEqual(actual, expected, message);
|
|
} else {
|
|
if (options.cloned)
|
|
QUnit.notStrictEqual(actual.id, expected.id,
|
|
message + ' (not #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.cloned && expected.name
|
|
? expected.name + ' 1' : expected.name,
|
|
message + ' (#name)');
|
|
compareProperties(actual, expected, ['children', 'bounds', 'position',
|
|
'matrix', 'data', 'opacity', 'locked', 'visible', 'blendMode',
|
|
'selected', 'fullySelected', 'clipMask', 'guide'],
|
|
message, options);
|
|
if (properties)
|
|
compareProperties(actual, expected, properties, message, options);
|
|
// Style
|
|
var styles = ['fillColor',
|
|
'strokeColor', 'strokeCap', 'strokeJoin', 'dashArray',
|
|
'dashOffset', 'miterLimit'];
|
|
if (expected instanceof TextItem)
|
|
styles.push('fontSize', 'font', 'leading', 'justification');
|
|
compareProperties(actual.style, expected.style, styles,
|
|
message + ' (#style)', options);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Run each callback in a separated canvas context and compare both outputs.
|
|
* This can be used to do selection drawing tests as it is not possible with
|
|
* comparePixels() method which relies on the item.rasterize() method which
|
|
* ignores selection.
|
|
* @param {number} width the width of the canvas
|
|
* @param {number} height the height of the canvas
|
|
* @param {function} expectedCallback the function producing the expected result
|
|
* @param {function} actualCallback the function producing the actual result
|
|
* @param {number} tolerance between 0 and 1
|
|
*/
|
|
var compareCanvas = function(width, height, expected, actual, tolerance) {
|
|
function getImageData(width, height, callback) {
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
var project = new Project(canvas);
|
|
var view = project.view;
|
|
callback();
|
|
view.update();
|
|
var imageData = view.context.getImageData(0, 0, width, height);
|
|
project.remove();
|
|
canvas.remove();
|
|
return imageData;
|
|
}
|
|
|
|
compareImageData(
|
|
getImageData(width, height, expected),
|
|
getImageData(width, height, actual),
|
|
tolerance
|
|
);
|
|
|
|
currentProject.activate();
|
|
};
|
|
|
|
// A list of comparator functions, based on `expected` type. See equals() for
|
|
// an explanation of how the type is determined.
|
|
var comparators = {
|
|
Null: QUnit.strictEqual,
|
|
Undefined: QUnit.strictEqual,
|
|
Boolean: QUnit.strictEqual,
|
|
|
|
Object: function(actual, expected, message, options) {
|
|
QUnit.push(Base.equals(actual, expected), actual, expected, message);
|
|
},
|
|
|
|
Element: function(actual, expected, message, options) {
|
|
// Convention: Loop through the attribute lists of both actual and
|
|
// expected element, and compare values even if they may be inherited.
|
|
// This is to handle styling values on SVGElement items more flexibly.
|
|
equals(actual && actual.tagName, expected.tagName,
|
|
(message || '') + ' (#tagName)', options);
|
|
for (var i = 0; i < expected.attributes.length; i++) {
|
|
var attr = expected.attributes[i];
|
|
if (attr.specified) {
|
|
equals(actual && actual.getAttribute(attr.name), attr.value,
|
|
(message || '') + ' (#' + attr.name + ')', options);
|
|
}
|
|
}
|
|
for (var i = 0; i < actual && actual.attributes.length; i++) {
|
|
var attr = actual.attributes[i];
|
|
if (attr.specified) {
|
|
equals(attr.value, expected.getAttribute(attr.name)
|
|
(message || '') + ' #(' + attr.name + ')', options);
|
|
}
|
|
}
|
|
},
|
|
|
|
Base: function(actual, expected, message, options) {
|
|
comparators.Object(actual, expected, message, options);
|
|
},
|
|
|
|
Number: function(actual, expected, message, options) {
|
|
// Compare with a default tolerance of 1e-5:
|
|
var ok = Math.abs(actual - expected)
|
|
<= Base.pick(options && options.tolerance, 1e-5);
|
|
QUnit.push(ok, ok ? expected : actual, expected, message);
|
|
},
|
|
|
|
Array: function(actual, expected, message, options) {
|
|
QUnit.strictEqual(actual.length, expected.length, message
|
|
+ ' (#length)');
|
|
for (var i = 0, l = actual.length; i < l; i++) {
|
|
equals(actual[i], expected[i], (message || '') + '[' + i + ']',
|
|
options);
|
|
}
|
|
},
|
|
|
|
Point: function(actual, expected, message, options) {
|
|
comparators.Number(actual.x, expected.x, message + ' (#x)', options);
|
|
comparators.Number(actual.y, expected.y, message + ' (#y)', options);
|
|
},
|
|
|
|
Size: function(actual, expected, message, options) {
|
|
comparators.Number(actual.width, expected.width,
|
|
message + ' (#width)', options);
|
|
comparators.Number(actual.height, expected.height,
|
|
message + ' (#height)', options);
|
|
},
|
|
|
|
Rectangle: function(actual, expected, message, options) {
|
|
comparators.Point(actual, expected, message, options);
|
|
comparators.Size(actual, expected, message, options);
|
|
},
|
|
|
|
Matrix: function(actual, expected, message, options) {
|
|
comparators.Array(actual.values, expected.values, message, options);
|
|
},
|
|
|
|
Color: function(actual, expected, message, options) {
|
|
if (actual && expected) {
|
|
equals(actual.type, expected.type, message + ' (#type)', options);
|
|
// NOTE: This also compares gradients, with identity checks and all.
|
|
equals(actual.components, expected.components,
|
|
message + ' (#components)', options);
|
|
} else {
|
|
QUnit.push(expected.equals(actual), actual, expected, message);
|
|
}
|
|
},
|
|
|
|
Segment: function(actual, expected, message, options) {
|
|
compareProperties(actual, expected, ['handleIn', 'handleOut', 'point',
|
|
'selected'], message, options);
|
|
},
|
|
|
|
SegmentPoint: function(actual, expected, message, options) {
|
|
comparators.Point(actual, expected, message, options);
|
|
comparators.Boolean(actual.selected, expected.selected,
|
|
message + ' (#selected)', options);
|
|
},
|
|
|
|
Item: compareItem,
|
|
|
|
Group: function(actual, expected, message, options) {
|
|
compareItem(actual, expected, message, options, ['clipped']);
|
|
},
|
|
|
|
Layer: function(actual, expected, message, options) {
|
|
compareItem(actual, expected, message, options);
|
|
var sameProject = actual.project === expected.project;
|
|
var sharedProject = !(options && options.dontShareProject);
|
|
QUnit.push(sharedProject ? sameProject : !sameProject,
|
|
actual.project,
|
|
sharedProject ? expected.project : 'not ' + expected.project,
|
|
message + ' (#project)');
|
|
},
|
|
|
|
Path: function(actual, expected, message, options) {
|
|
compareItem(actual, expected, message, options,
|
|
['segments', 'closed', 'clockwise']);
|
|
},
|
|
|
|
CompoundPath: function(actual, expected, message, options) {
|
|
compareItem(actual, expected, message, options);
|
|
},
|
|
|
|
Raster: function(actual, expected, message, options) {
|
|
var pixels = options && options.pixels,
|
|
properties = ['size', 'width', 'height', 'resolution'];
|
|
if (!pixels)
|
|
properties.push('source', 'image');
|
|
compareItem(actual, expected, message, options, properties);
|
|
if (pixels) {
|
|
comparePixels(actual, expected, message, options);
|
|
} else {
|
|
equals(actual.toDataURL(), expected.toDataURL(),
|
|
message + ' (#toDataUrl())');
|
|
}
|
|
},
|
|
|
|
Shape: function(actual, expected, message, options) {
|
|
compareItem(actual, expected, message, options,
|
|
['shape', 'size', 'radius']);
|
|
},
|
|
|
|
PointText: function(actual, expected, message, options) {
|
|
compareItem(actual, expected, message, options,
|
|
['content', 'point']);
|
|
},
|
|
|
|
SymbolItem: function(actual, expected, message, options) {
|
|
compareItem(actual, expected, message,
|
|
// Cloning SymbolItems does not result in cloned
|
|
// SymbolDefinitions
|
|
options && options.cloned
|
|
? Base.set({}, options, { cloned: false })
|
|
: options,
|
|
['symbol']);
|
|
},
|
|
|
|
SymbolDefinition: function(actual, expected, message, options) {
|
|
equals(actual.definition, expected.definition,
|
|
message + ' (#definition)', options);
|
|
},
|
|
|
|
Project: function(actual, expected, message, options) {
|
|
compareProperties(actual, expected, ['layers'], 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 compareBoolean = function(actual, expected, message, options) {
|
|
expected = typeof expected === 'string'
|
|
? PathItem.create(expected)
|
|
: expected;
|
|
if (typeof actual === 'function') {
|
|
if (!message)
|
|
message = getFunctionMessage(actual);
|
|
actual = actual();
|
|
}
|
|
var parent,
|
|
index,
|
|
style = {
|
|
strokeColor: 'black',
|
|
fillColor: expected && (expected.closed
|
|
|| expected.firstChild && expected.firstChild.closed && 'yellow')
|
|
|| null
|
|
};
|
|
if (actual) {
|
|
parent = actual.parent;
|
|
index = actual.index;
|
|
// Remove it from parent already now, in case we're comparing children
|
|
// of compound-paths, so we can apply styling to them.
|
|
if (parent && parent instanceof CompoundPath) {
|
|
actual.remove();
|
|
} else {
|
|
parent = null;
|
|
}
|
|
actual.style = style;
|
|
}
|
|
if (expected) {
|
|
expected.style = style;
|
|
}
|
|
equals(actual, expected, message, Base.set({ rasterize: true }, options));
|
|
if (parent) {
|
|
// Insert it back.
|
|
parent.insertChild(index, actual);
|
|
}
|
|
};
|
|
|
|
var createSVG = function(str, attrs) {
|
|
// Similar to SvgElement.create():
|
|
var node = document.createElementNS('http://www.w3.org/2000/svg', str);
|
|
for (var key in attrs)
|
|
node.setAttribute(key, attrs[key]);
|
|
// Paper.js paths do not have a fill by default, SVG does.
|
|
node.setAttribute('fill', 'none');
|
|
return node;
|
|
};
|
|
|
|
var compareSVG = function(done, actual, expected, message, options) {
|
|
function getItem(item) {
|
|
return item instanceof Item
|
|
? item
|
|
: typeof item === 'string'
|
|
? new Raster({
|
|
source: 'data:image/svg+xml;base64,' + window.btoa(item),
|
|
insert: false
|
|
})
|
|
: null;
|
|
}
|
|
|
|
actual = getItem(actual);
|
|
expected = getItem(expected);
|
|
actual.position = expected.position;
|
|
|
|
if (typeof actual === 'function') {
|
|
if (!message)
|
|
message = getFunctionMessage(actual);
|
|
actual = actual();
|
|
}
|
|
|
|
function compare() {
|
|
comparePixels(actual, expected, message, Base.set({
|
|
tolerance: 1e-3,
|
|
resolution: 72
|
|
}, options));
|
|
done();
|
|
}
|
|
|
|
if (expected instanceof Raster) {
|
|
expected.onLoad = compare;
|
|
} else if (actual instanceof Raster) {
|
|
actual.onLoad = compare;
|
|
} else {
|
|
compare();
|
|
}
|
|
};
|
|
|
|
|
|
//
|
|
// Interactions helpers
|
|
//
|
|
var MouseEventPolyfill = function(type, params) {
|
|
var mouseEvent = document.createEvent('MouseEvent');
|
|
mouseEvent.initMouseEvent(
|
|
type,
|
|
params.bubbles,
|
|
params.cancelable,
|
|
window,
|
|
0,
|
|
params.screenX,
|
|
params.screenY,
|
|
params.clientX,
|
|
params.clientY,
|
|
params.ctrlKey,
|
|
params.altKey,
|
|
params.shiftKey,
|
|
params.metaKey,
|
|
params.button,
|
|
params.relatedTarget
|
|
);
|
|
return mouseEvent;
|
|
};
|
|
|
|
MouseEventPolyfill.prototype = nativeClasses.Event.prototype;
|
|
|
|
var triggerMouseEvent = function(type, point, target) {
|
|
// Depending on event type, events have to be triggered on different
|
|
// elements due to the event handling implementation (see `viewEvents`
|
|
// and `docEvents` in View.js). And we cannot rely on the fact that event
|
|
// will bubble from canvas to document, since the canvas used in tests is
|
|
// not inserted in DOM.
|
|
target = target || (type === 'mousedown' ? view.element : document);
|
|
// If `gulp load` was run, there is a name collision between paper Event /
|
|
// MouseEvent and native javascript classes. In this case, we need to use
|
|
// native classes stored in the nativeClasses object instead.
|
|
// MouseEvent class does not exist in PhantomJS, so in that case, we need to
|
|
// use a polyfill method, see: https://stackoverflow.com/questions/42929639
|
|
var MouseEvent = typeof nativeClasses.MouseEvent === 'function'
|
|
? nativeClasses.MouseEvent
|
|
: MouseEventPolyfill;
|
|
var event = new MouseEvent(type, {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
composed: true,
|
|
clientX: point.x,
|
|
clientY: point.y,
|
|
screenX: point.x,
|
|
screenY: point.y
|
|
});
|
|
target.dispatchEvent(event);
|
|
};
|