diff --git a/src/load.js b/src/load.js index 4d14ea3a..59e08c7c 100644 --- a/src/load.js +++ b/src/load.js @@ -14,7 +14,7 @@ // the browser, avoiding the step of having to manually preprocess it after each // change. This is very useful during development of the library itself. if (typeof window === 'object') { - // Browser based loading through Prepro.js: + // Browser based loading through Prepro.js: if (!window.include) { // Get the last script tag and assume it's the one that loaded this file // then get its src attribute and figure out the location of our root. @@ -36,6 +36,13 @@ if (typeof window === 'object') { // the code the 2nd time around. load(root + 'src/load.js'); } else { + // Some native javascript classes have name collisions with Paper.js + // classes. Store them to be able to use them later in tests. + NativeClasses = { + Event: Event, + MouseEvent: MouseEvent + }; + include('options.js'); // Load constants.js, required by the on-the-fly preprocessing: include('constants.js'); diff --git a/src/view/View.js b/src/view/View.js index e7edb3eb..935989a6 100644 --- a/src/view/View.js +++ b/src/view/View.js @@ -1498,7 +1498,31 @@ new function() { // Injection scope for event handling on the browser * Loops through all views and sets the focus on the first * active one. */ - updateFocus: updateFocus + updateFocus: updateFocus, + + /** + * Clear all events handling state informations. Made for testing + * purpose, to have a way to start with a fresh state before each + * test. + * @private + */ + _clearState: function() { + prevFocus = null; + tempFocus = null; + dragging = false; + mouseDown = false; + called = false; + wasInView = false; + overView = null; + downPoint = null; + lastPoint = null; + downItem = null; + overItem = null; + dragItem = null; + clickItem = null; + clickTime = null; + dblClick = null; + } } }; }); diff --git a/test/helpers.js b/test/helpers.js index 20f1b13e..1cfd30b3 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -48,12 +48,16 @@ console.error = function() { errorHandler.apply(this, arguments); }; +var currentProject; + QUnit.done(function(details) { console.error = errorHandler; + // Clear all event listeners after final test. + if (currentProject) { + currentProject.remove(); + } }); -var currentProject; - // 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! @@ -61,9 +65,16 @@ 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) + if (currentProject) { currentProject.remove(); - currentProject = new Project(); + // This is needed for interactions tests, to make sure that test is + // run with a fresh state. + View._clearState(); + } + + // Instantiate project with 100x100 pixels canvas instead of default + // 1x1 to make interactions tests simpler by working with integers. + currentProject = new Project(CanvasProvider.getCanvas(100, 100)); expected(assert); }); }; @@ -550,3 +561,63 @@ var compareSVG = function(done, actual, expected, message, options) { 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 = typeof NativeClasses !== 'undefined' + && NativeClasses.Event.prototype || 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 global NativeClasses object instead. + var constructor = typeof NativeClasses !== 'undefined' + && NativeClasses.MouseEvent || MouseEvent; + + // MouseEvent class does not exist in PhantomJS, so in that case, we need to + // use a polyfill method. + if (typeof constructor !== 'function') { + constructor = MouseEventPolyfill; + } + + var event = new constructor(type, { + bubbles: true, + cancelable: true, + composed: true, + clientX: point.x, + clientY: point.y, + screenX: point.x, + screenY: point.y + }); + target.dispatchEvent(event); +}; diff --git a/test/tests/Interactions.js b/test/tests/Interactions.js new file mode 100644 index 00000000..495c8a57 --- /dev/null +++ b/test/tests/Interactions.js @@ -0,0 +1,336 @@ +/* + * 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. + */ + +/** + * These tests are focused on user interactions. + * They trigger events and check callbacks. + * Warning: when running tests locally from `gulp test:browser` command, don't + * move your mouse over the window because that could perturbate tests + * execution. + */ +QUnit.module('Interactions'); + +// +// Mouse +// +test('Item#onMouseDown()', function(assert) { + var done = assert.async(); + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + var point = new Point(5, 5); + item.onMouseDown = function(event) { + equals(event.type, 'mousedown'); + equals(event.point, point); + equals(event.target, item); + equals(event.currentTarget, item); + equals(event.delta, null); + done(); + }; + triggerMouseEvent('mousedown', point); +}); + +test('Item#onMouseDown() with stroked item', function(assert) { + var done = assert.async(); + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.strokeColor = 'red'; + var point = new Point(0, 0); + item.onMouseDown = function(event) { + equals(event.type, 'mousedown'); + equals(event.point, point); + equals(event.target, item); + equals(event.currentTarget, item); + equals(event.delta, null); + done(); + }; + triggerMouseEvent('mousedown', point); +}); + +test('Item#onMouseDown() is not triggered when item is not filled', function(assert) { + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.onMouseDown = function(event) { + throw 'this should not be called'; + }; + triggerMouseEvent('mousedown', new Point(5, 5)); + expect(0); +}); + +test('Item#onMouseDown() is not triggered when item is not visible', function(assert) { + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + item.visible = false; + item.onMouseDown = function(event) { + throw 'this should not be called'; + }; + triggerMouseEvent('mousedown', new Point(5, 5)); + expect(0); +}); + +test('Item#onMouseDown() is not triggered when item is locked', function(assert) { + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + item.locked = true; + item.onMouseDown = function(event) { + throw 'this should not be called'; + }; + triggerMouseEvent('mousedown', new Point(5, 5)); + expect(0); +}); + +test('Item#onMouseDown() is not triggered when another item is in front', function(assert) { + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + var item2 = item.clone(); + item.onMouseDown = function(event) { + throw 'this should not be called'; + }; + triggerMouseEvent('mousedown', new Point(5, 5)); + expect(0); +}); + +test('Item#onMouseDown() is not triggered if event target is document', function(assert) { + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + item.onMouseDown = function(event) { + throw 'this should not be called'; + }; + triggerMouseEvent('mousedown', new Point(5, 5), document); + expect(0); +}); + +test('Item#onMouseMove()', function(assert) { + var done = assert.async(); + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + var point = new Point(5, 5); + item.onMouseMove = function(event) { + equals(event.type, 'mousemove'); + equals(event.point, point); + equals(event.target, item); + equals(event.currentTarget, item); + equals(event.delta, null); + done(); + }; + triggerMouseEvent('mousemove', point); +}); + +test('Item#onMouseMove() is not re-triggered if point is the same', function(assert) { + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + var point = new Point(5, 5); + var counter = 0; + item.onMouseMove = function(event) { + equals(true, true); + }; + triggerMouseEvent('mousemove', point); + triggerMouseEvent('mousemove', point); + expect(1); +}); + +test('Item#onMouseUp()', function(assert) { + var done = assert.async(); + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + var point = new Point(5, 5); + item.onMouseUp = function(event) { + equals(event.type, 'mouseup'); + equals(event.point, point); + equals(event.target, item); + equals(event.currentTarget, item); + equals(event.delta, new Point(0, 0)); + done(); + }; + triggerMouseEvent('mousedown', point); + triggerMouseEvent('mouseup', point); +}); + +test('Item#onMouseUp() is only triggered after mouse down', function(assert) { + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + item.onMouseUp = function(event) { + throw 'this should not be called'; + }; + triggerMouseEvent('mouseup', new Point(5, 5)); + expect(0); +}); + +test('Item#onClick()', function(assert) { + var done = assert.async(); + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + var point = new Point(5, 5); + item.onClick = function(event) { + equals(event.type, 'click'); + equals(event.point, point); + equals(event.target, item); + equals(event.currentTarget, item); + equals(event.delta, new Point(0, 0)); + done(); + }; + triggerMouseEvent('mousedown', point); + triggerMouseEvent('mouseup', point); +}); + +test('Item#onClick() is not triggered if up point is not on item', function(assert) { + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + item.onClick = function(event) { + throw 'this should not be called'; + }; + triggerMouseEvent('mousedown', new Point(5, 5)); + triggerMouseEvent('mouseup', new Point(15, 15)); + expect(0); +}); + +test('Item#onClick() is not triggered if down point is not on item', function(assert) { + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + item.onClick = function(event) { + throw 'this should not be called'; + }; + triggerMouseEvent('mousedown', new Point(15, 15)); + triggerMouseEvent('mouseup', new Point(5, 5)); + expect(0); +}); + +test('Item#onDoubleClick()', function(assert) { + var done = assert.async(); + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + var point = new Point(5, 5); + item.onDoubleClick = function(event) { + equals(event.type, 'doubleclick'); + equals(event.point, point); + equals(event.target, item); + equals(event.currentTarget, item); + equals(event.delta, new Point(0, 0)); + done(); + }; + triggerMouseEvent('mousedown', point); + triggerMouseEvent('mouseup', point); + triggerMouseEvent('mousedown', point); + triggerMouseEvent('mouseup', point); +}); + +test('Item#onDoubleClick() is not triggered if both clicks are not on same item', function(assert) { + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + var item2 = item.clone().translate(5); + item.onDoubleClick = function(event) { + throw 'this should not be called'; + }; + triggerMouseEvent('mousedown', new Point(5, 5)); + triggerMouseEvent('mouseup', new Point(5, 5)); + triggerMouseEvent('mousedown', new Point(6, 6)); + triggerMouseEvent('mouseup', new Point(6, 6)); + expect(0); +}); + +test('Item#onDoubleClick() is not triggered if time between both clicks is too long', function(assert) { + var done = assert.async(); + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + var point = new Point(5, 5); + item.onDoubleClick = function(event) { + throw 'this should not be called'; + }; + triggerMouseEvent('mousedown', point); + triggerMouseEvent('mouseup', point); + setTimeout(function() { + triggerMouseEvent('mousedown', point); + triggerMouseEvent('mouseup', point); + done(); + }, 301); + expect(0); +}); + +test('Item#onMouseEnter()', function(assert) { + var done = assert.async(); + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + var point = new Point(5, 5); + item.onMouseEnter = function(event) { + equals(event.type, 'mouseenter'); + equals(event.point, point); + equals(event.target, item); + equals(event.currentTarget, item); + equals(event.delta, null); + done(); + }; + triggerMouseEvent('mousemove', point); +}); + +test('Item#onMouseEnter() is only re-triggered after mouse leave', function(assert) { + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + item.onMouseEnter = function(event) { + equals(true, true); + }; + // enter + triggerMouseEvent('mousemove', new Point(5, 5)); + triggerMouseEvent('mousemove', new Point(6, 6)); + triggerMouseEvent('mousemove', new Point(7, 7)); + // leave + triggerMouseEvent('mousemove', new Point(11, 11)); + // re-enter + triggerMouseEvent('mousemove', new Point(10, 10)); + expect(2); +}); + +test('Item#onMouseLeave()', function(assert) { + var done = assert.async(); + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + var point1 = new Point(5, 5); + var point2 = new Point(15, 15); + item.onMouseLeave = function(event) { + equals(event.type, 'mouseleave'); + equals(event.point, point2); + equals(event.target, item); + equals(event.currentTarget, item); + equals(event.delta, null); + done(); + }; + triggerMouseEvent('mousemove', point1); + triggerMouseEvent('mousemove', point2); +}); + +test('Item#onMouseDrag()', function(assert) { + var done = assert.async(); + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + var point1 = new Point(5, 5); + var point2 = new Point(15, 15); + item.onMouseDrag = function(event) { + equals(event.type, 'mousedrag'); + equals(event.point, point2); + equals(event.target, item); + equals(event.currentTarget, item); + equals(event.delta, new Point(10, 10)); + done(); + }; + triggerMouseEvent('mousedown', point1); + triggerMouseEvent('mousemove', point2); +}); + +test('Item#onMouseDrag() is not triggered after mouse up', function(assert) { + var item = new Path.Rectangle(new Point(0, 0), new Size(10)); + item.fillColor = 'red'; + item.onMouseDrag = function(event) { + equals(true, true); + }; + triggerMouseEvent('mousedown', new Point(5, 5)); + triggerMouseEvent('mousemove', new Point(6, 6)); + triggerMouseEvent('mouseup', new Point(7, 7)); + triggerMouseEvent('mousemove', new Point(8, 8)); + expect(1); +}); + diff --git a/test/tests/load.js b/test/tests/load.js index 35c369c4..f0395192 100644 --- a/test/tests/load.js +++ b/test/tests/load.js @@ -62,3 +62,6 @@ /*#*/ include('SvgExport.js'); /*#*/ include('Numerical.js'); + +// There is no need to test interactions in node context. +if (!isNode) /*#*/ include('Interactions.js');