Add mouse interaction tests

- Tests common mouse interactions scenarios to prevent regressions
when making changes. These tests are not run in node context.
- Prevent name collision between Javascript native classes and Paper.js
classes (Event and MouseEvent) by patching load.js.
- Uses a polyfill for MouseEvent which is missing in PhantomJS.
- Adds View._clearState() method and use it in tests to make sure that
each new test is started with a fresh state.
This commit is contained in:
sasensi 2018-10-15 12:06:04 +02:00 committed by Jürg Lehni
parent 44a31c9399
commit 1bd67b2d9b
5 changed files with 447 additions and 6 deletions

View file

@ -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');

View file

@ -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;
}
}
};
});

View file

@ -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);
};

336
test/tests/Interactions.js Normal file
View file

@ -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);
});

View file

@ -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');