When creating automated tests for your application or modules, you need to be able to mock user events. The DOM supports creating native events that behave essentially the same as user generated events, though without the associated browser default behaviors (e.g. following a link on click).
The event-simulate
module adds the Y.Event.simulate
method for
working with raw DOM nodes, but for most cases, the
node-event-simulate
module is the right choice, since it allows you
to call the simulate
method directly from the Node
.
There are seven mouse events that can be simulated:
click
dblclick
mousedown
mouseup
mouseover
mouseout
mousemove
Each event is fired by calling simulate()
and passing in two
arguments: the type of event to fire and an optional object specifying
additional information for the event. To simulate a click on the document's
body element, for example, the following code can be used:
YUI().use('node-event-simulate', function(Y) { Y.one("body").simulate("click"); });
This code simulates a click with all of the default properties on the
event
object. To specify additional information, such as the
Shift key being down, the second argument must be used and the exact DOM
name for the event property specified (there is browser-normalizing logic
that translates these into browser-specific properties when necessary):
Y.one("body").simulate("click", { shiftKey: true });
In this updated example, a click event is fired on the document's body while simulating that the Shift key is down.
The extra properties to specify vary depending on the event being simulated and are limited to this list:
detail
screenX
, screenY
clientX
, clientY
ctrlKey
, altKey
, shiftKey
, metaKey
button
relatedTarget
mouseover
event) or to
(during a mouseout
event).
YUI().use('node-event-simulate', function(Y) { var node = Y.one("#myDiv"); //simulate a click Alt key down node.simulate("click", { altKey: true}); //simulate a double click with Ctrl key down node.simulate("dblclick", { ctrlKey: true }); //simulate a mouse over node.simulate("mouseover", { relatedTarget: document.body }); //simulate a mouse out node.simulate("mouseout", { relatedTarget: document.body }); //simulate a mouse down at point (100,100) in the client area node.simulate("mousedown", { clientX: 100, clientY: 100 }); //simulate a mouse up at point (100,100) in the client area node.simulate("mouseup", { clientX: 100, clientY: 100 }); //simulate a mouse move at point (200, 200) in the client area node.simulate("mousemove", { clientX: 200, clientY: 200 }); });
There are three key event simulations available:
keyup
keydown
keypress
As with the mouse events, key events are simulated using
simulate()
. For keyup
and keydown
,
the keyCode
property must be specified; for
keypress
, the charCode
property must be included.
In many cases, keyCode
and charCode
may be the
same value to represent the same key (97, for instance, represents the
"A" key as well as being the ASCII code for the letter
"a"). For example:
YUI().use('node-event-simulate', function(Y) { var node = Y.one("#myDiv"); //simulate a keydown on the A key node.simulate("keydown", { keyCode: 97 }); //simulate a keyup on the A key node.simulate("keyup", { keyCode: 97 }); //simulate typing "a" node.simulate("keypress", { charCode: 97 }); });
Key events also support the ctrlKey
, altKey
,
shiftKey
, and metaKey
event properties.
Note: Due to differences in browser implementations, key
events may not be simulated in the same manner across all browsers. For
instance, when simulating a keypress event on a textbox, only Firefox will
update the textbox with the new character of the key that was simulated to
be pressed. For other browsers, the events are still registered and all
event handlers are called, however, the textbox display and
value
property are not updated. These differences should go
away as browser support for simulated events improves in the future.
There are several UI event simulations available:
blur
change
focus
resize
scroll
select
As with the other events, UI events are simulated using
simulate()
. There are no properties that are required to
simulate UI events as these events don't carry extra information. Some
examples:
YUI().use('node-event-simulate', function(Y) { var node = Y.one("#myInput"); //simulate a change event node.simulate("change"); //simulate a select event node.simulate("select"); });
There are several high level gesture simulations primarily targeted for
smartphones, tablets, and other touch-enabled devices. Single touch gestures
such as tap
and flick
are simulated using Mouse Events on desktop or mobile
devices where creating Touch Events are not supported. All gesture simulations
are done by calling the simulateGesture()
method against a Node instance. The
method is asynchronous by default so an optional callback function can be
passed.
tap
doubletap
press
move
flick
pinch
rotate
Gesture can be simulated by calling simulateGesture()
and
passing the following arguments: the name of the gesture to simulate, an
optional object to define gesture properties, and an optional callback function.
The available properties vary per gesture.
If the location of the finger is not given, the center of the node element is used by default. This default behavior can be overridden by passing coordinates into the optional object. The coordinate values are relative to the top/left corner of the node element.
The following code simulates tap, double tap and press gestures:
YUI().use('node-event-simulate', function(Y) { var node = Y.one("#myElement"); //simulate tap gesture node.simulateGesture("tap"); //simulate double-tap gesture node.simulateGesture("doubletap"); //simulate press gesture node.simulateGesture("press", { hold: 3000 // press and hold for 3000ms }); // simulate tap with options and callback node.simulateGesture("tap", { point: [30, 30], // tap (30, 30) relative to the top/left of the node hold: 3000, // hold for 3sec in a tap times: 2, // tap 2 times delay: 500 // delay time before next tap starts }, function() { Y.log("I was called."); }); });
Optional properties for the tap
gesture:
point
hold
touchstart
and touchend
event generation.
times
delay
times
is more than 1.
Optional properties for the doubletap
gesture:
point
The press
gesture is essentially a single tap with the hold
property
defined. Optional properties for the press
gesture:
point
hold
touchstart
and touchend
event generation.
The following code can be used To simulate various gestures of moving one finger:
YUI().use('node-event-simulate', function(Y) { var node = Y.one("#myElement"); //simulate moving a finger 200 pixels along the x-axis //to the right for one second (default) node.simulateGesture("move"); //simulate moving a finger from the center of the node //to a point 70 pixels to the right and 50 pixels down over 2000ms node.simulateGesture("move", { path: { xdist: 70, ydist: -50 } , duration: 2000 }); //simulate a flick to the right (default) node.simulateGesture("flick"); //simulate a flick down 100 pixels over 50ms node.simulateGesture("flick", { axis: "y", distance: -100, duration: 50 }); });
Optional properties for the move
gesture:
path
point
, xdist
and ydist
. The point
is
the start point and defaults to the center of the node element.
xdist
and ydist
indicate the distance moved in pixels along the
x- and y-axis. A negative distance value indicates moving to left
for xdist
and down for ydist
.
duration
Optional properties for the flick
gesture:
point
axis
distance
duration
The pinch
gesture is used to simulate the pinching and spreading of two
fingers. During a pinch simulation, rotation is also possible. Essentially
pinch
and rotate
simulations share the same base implementation to allow
both pinching and rotation at the same time. The only difference is pinch
requires start
and end
option properties while rotate
requires rotation
option property.
The pinch
and rotate
gestures can be described as placing 2 fingers along a
circle. Pinching and spreading can be described by start and end circles while
rotation occurs on a single circle. If the radius of the start circle is greater
than the end circle, the gesture becomes a pinch, otherwise it is a spread spread.
The following code can be used to simulate two finger gestures:
YUI().use('node-event-simulate', function(Y) { var node = Y.one("#myElement"); //simulate a pinch: "r1" and "r2" are required node.simulateGesture("pinch", { r1: 100, // start circle radius at the center of the node r2: 50 // end circle radius at the center of the node }); //simulate a spread: same as "pinch" gesture but r2>r1 node.simulateGesture("pinch", { r1: 50, r2: 100 }); //simulate rotating a node 75 degrees counter-clockwise node.simulateGesture("rotate", { rotation: -75 }); //simulate a pinch and a rotation at the same time. //fingers start on a circle of radius 100 px, placed at top/bottom //fingers end on a circle of radius 50px, placed at right/left node.simulateGesture("pinch", { r1: 100, r2: 50, start: 0 rotation: 90 }); });
The optional properties available for pinch
and rotate
gestures are the
same:
center
r1
pinch
gestures but optional for rotate
. Pixel radius
of the start circle. If omitted in rotate
gestures, default is
a fourth of the node element width or height, whichever is smaller.
r2
pinch
gestures but optional for rotate
gestures.
Pixel radius of the end circle. If omitted in rotate
gestures,
default is a fourth of the node element width or height, whichever
is smaller.
duration
start
rotation
If the gesture simulation is called in iOS, it generates not only touch events
but also
iOS specific gesture events: gesturestart
, gesturechange
and gestureend
.
You can define custom default behaviors for gesture simulations by modifying the
following Y.GestureSimulation.defaults
object properties:
And an example:
YUI().use('node-event-simulate', function(Y) { var node = Y.one("#myElement"); Y.GestureSimulation.defaults = Y.merge(Y.GestureSimulation.defaults, { HOLD_TAP: 3000 }); //now touchend event will be generated after 3 sec from the touchstart //event generation. node.simulateGesture("tap"); });
Event simulation is for automated testing. Your application should respond to real user events. For reasons mentioned below, it can be easy to get your application into a confused runtime state when some callbacks have been executed but not others.
Typically, event simulation is sought to trigger certain callbacks. If a function needs to respond to user action or be called programmatically, it should be written accordingly and called directly in the latter case. Often a better solution is to extract the core logic from the event handler into a separate function and call that method from the event handler and from the other part of the application that was going to use simulation.
In some cases, simulation is wanted because there may be any number of subscriptions on a node, and all applicable callbacks should be triggered. If this is the case, investigate using custom events, instead.
The bottom line is, reliance on event simulation in production code is a warning sign that the architecture is not scaling. The affected code should be refactored before it becomes a larger problem.
In many cases, events happen in groups (mousedown
, mouseup
, click
, or
keydown
, keyup
, keypress
). If you simulate an event that is
typically part of a group or is often followed by other events, the
other events will NOT be generated for free.
For example, if you simulate a click
event on a submit button, you only
simulate the click
event. The preceding mousedown
and mouseup
, as
well as the subsequently expected 'submit' are neither simulated or fired
natively.
The Synthetic event system doesn't yet support defining simulation. In most cases, though, synthetic events are triggered by other DOM events that can be simulated, so it's often possible to trigger them by simulating the underlying events. But that ignores the point that synthetic events are supposed to mask that abstraction for your benefit.
Support for synthetic event simulation is on the roadmap.