siesta-lite
Version:
Stress-free JavaScript unit testing and functional testing tool, works in NodeJS and browsers
659 lines (500 loc) • 24.5 kB
HTML
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>The source code</title>
<link href="../resources/prettify/prettify.css" type="text/css" rel="stylesheet" />
<script type="text/javascript" src="../resources/prettify/prettify.js"></script>
<style type="text/css">
.highlight { display: block; background-color: #ddd; }
</style>
<script type="text/javascript">
function highlight() {
document.getElementById(location.hash.replace(/#/, "")).className = "highlight";
}
</script>
</head>
<body onload="prettyPrint(); highlight();">
<pre class="prettyprint lang-js">/*
Siesta 5.6.1
Copyright(c) 2009-2022 Bryntum AB
https://bryntum.com/contact
https://bryntum.com/products/siesta/license
*/
// First fire a mouseover + mousemove on the target
// Based on Chrome's behavior
(function () {
var postTapSequence = ['mouseover', /*'mouseenter'*/, 'mousemove', 'mousedown', 'mouseup', 'click'];
var postLongPressSequence = ['mouseover', /*'mouseenter'*/, 'mousemove', 'contextmenu'];
var postLongPressSequenceWithTouchStartPrevented = ['mousemove', 'contextmenu'];
Role('Siesta.Test.Simulate.Touch', {
requires : [],
has : {
touchEventNamesMap : {
lazy : 'this.buildTouchEventNamesMap'
},
currentTouchId : 1,
activeTouches : Joose.I.Object,
longPressDelay : {
init : 1500,
is : 'rw'
}
},
methods : {
simulateTap : function (context, options) {
var queue = new Siesta.Util.Queue({
deferer : this.test.originalSetTimeout,
deferClearer : this.test.originalClearTimeout,
interval : 30,
observeTest : this.test
})
var me = this;
var id
queue.addStep({
processor : function () {
id = me.touchStart(null, null, options, context)
}
})
queue.addStep({
processor : function () {
me.touchEnd(id, options)
}
})
return new Promise(function (resolve, reject) {
queue.run(resolve)
})
},
simulateDoubleTap : function (context, options) {
var queue = new Siesta.Util.Queue({
deferer : this.test.originalSetTimeout,
deferClearer : this.test.originalClearTimeout,
interval : 30,
observeTest : this.test
})
var me = this;
var id
queue.addStep({
processor : function () {
id = me.touchStart(null, null, options, context)
}
})
queue.addStep({
processor : function () {
me.touchEnd(id, options)
}
})
queue.addStep({
processor : function () {
id = me.touchStart(null, null, options, context)
}
})
queue.addStep({
processor : function () {
me.touchEnd(id, options)
// iOS Safari fires dblclick event
me.simulateEvent([], 'dblclick', options);
}
})
return new Promise(function (resolve, reject) {
queue.run(resolve)
})
},
simulateLongPress : function (context, options) {
var queue = new Siesta.Util.Queue({
deferer : this.test.originalSetTimeout,
deferClearer : this.test.originalClearTimeout,
interval : 30,
observeTest : this.test
})
var me = this;
var id
queue.addStep({
processor : function () {
id = me.touchStart(null, null, options, context)
}
})
queue.addDelayStep(this.getLongPressDelay())
queue.addStep({
processor : function () {
me.touchEnd(id, options)
}
})
return new Promise(function (resolve, reject) {
queue.run(resolve)
})
},
simulatePinch : function (context1, context2, options) {
var queue = new Siesta.Util.Queue({
deferer : this.test.originalSetTimeout,
deferClearer : this.test.originalClearTimeout,
interval : 30,
observeTest : this.test
})
var id1, id2
var dx = context1.localXY[ 0 ] - context2.localXY[ 0 ]
var dy = context1.localXY[ 1 ] - context2.localXY[ 1 ]
var distance = Math.sqrt(dx * dx + dy * dy)
if (distance < 1) distance = 1
var scaled = distance * scale
var delta = (scaled - distance) / 2
var angle = Math.atan(dy / dx)
var x1 = Math.round(context1.localXY[ 0 ] - delta * Math.cos(angle))
var y1 = Math.round(context1.localXY[ 1 ] - delta * Math.sin(angle))
var x2 = Math.round(context2.localXY[ 0 ] + delta * Math.cos(angle))
var y2 = Math.round(context2.localXY[ 1 ] + delta * Math.sin(angle))
var options2 = Joose.O.extend({}, options)
queue.addStep({
processor : function () {
id1 = me.touchStart(null, null, options, context1)
id2 = me.touchStart(null, null, options2, context2)
}
})
queue.addAsyncStep({
processor : function (data) {
var move1Done = false
var move2Done = false
me.touchMove(id1, x1, y1, function () {
move1Done = true
if (move1Done && move2Done) data.next()
}, null, options)
me.touchMove(id2, x2, y2, function () {
move2Done = true
if (move1Done && move2Done) data.next()
}, null, options2)
}
})
queue.addStep({
processor : function () {
me.touchEnd(id1, options)
me.touchEnd(id2, options2)
}
})
return new Promise(function (resolve, reject) {
queue.run(resolve)
})
},
simulateTouchDrag : function (sourceXY, targetXY, options, dragOnly) {
var me = this
options = options || {};
// For drag operations we should always use the top level document.elementFromPoint
var source = me.elementFromPoint(sourceXY[ 0 ], sourceXY[ 1 ], true);
var target = me.elementFromPoint(targetXY[ 0 ], targetXY[ 1 ], true);
var queue = new Siesta.Util.Queue({
deferer : this.test.originalSetTimeout,
deferClearer : this.test.originalClearTimeout,
interval : me.dragDelay,
callbackDelay : me.afterActionDelay,
observeTest : this.test
});
var id
queue.addStep({
processor : function () {
id = me.touchStart(sourceXY, null, options, null)
}
})
queue.addAsyncStep({
processor : function (data) {
me.touchMove(id, targetXY[ 0 ], targetXY[ 1 ], options).then(data.next)
}
})
queue.addStep({
processor : function () {
// if `dragOnly` flag is set, do not finalize the touch, instead, pass the touch id
// to the user in the callback (see below)
if (!dragOnly) me.touchEnd(id, options, true)
}
})
return new Promise(function (resolve, reject) {
queue.run(function () {
// if `dragOnly` flag is set pass the touch id as promise result
if (dragOnly)
resolve(id)
else
resolve()
})
})
},
touchStart : function (target, offset, options, context) {
if (!context) context = this.test.getNormalizedTopElementInfo(target, true, 'touchStart', offset)
options = Joose.O.extend({
clientX : context.localXY[0],
clientY : context.localXY[1]
}, options || {})
var event = this.simulateTouchEventGeneric(context.el, 'start', options, null)
this.lastStartTouchWasOnNewTarget = !this.lastStartTouch || this.lastStartTouch.target !== context.el;
this.lastStartTouchEvent = event;
// IE11 compat check
this.lastStartTouch = event.touches ? event.touches[0] : event;
this.lastStartTouchTimeStamp = Date.now();
return event.pointerId != null ? event.pointerId : event.changedTouches[ 0 ].identifier
},
touchEnd : function (touchId, options) {
touchId = touchId || Object.keys(this.activeTouches)[0];
var touch = this.activeTouches[touchId]
if (!touch) throw "Can't find active touch: " + touchId
options = Joose.O.extend({
clientX : touch.clientX,
clientY : touch.clientY
}, options || {})
var target = touch.target
if (this.test.nodeIsOrphan(target)) {
touch.target = this.global.document.body
}
this.simulateTouchEventGeneric(touch.currentEl || touch.target, 'end', options, { touchId : touchId })
},
// Assumes an active touch exists
touchMoveTo : function (toXY, options) {
var touches = Object.keys(this.activeTouches);
if (touches.length === 0) {
throw new Error('No active touch detected');
}
var touch = this.activeTouches[ touches[0] ];
return this.touchMove(touches[0], toXY[0], toXY[1], options);
},
// Assumes an active touch exists
touchMoveBy : function (byXY, options) {
var touches = Object.keys(this.activeTouches);
if (touches.length === 0) {
throw new Error('No active touch detected');
}
var touch = this.activeTouches[ touches[0] ];
return this.touchMove(touches[0],this.currentPosition[0] + byXY[0], this.currentPosition[1] + byXY[1], options);
},
touchMove : function (touchId, toX, toY, options) {
var touch = this.activeTouches[ touchId ]
if (!touch) throw "Can't find active touch: " + touchId
var me = this
var overEls = []
return this.movePointerTemplate({
xy : [ touch.clientX, touch.clientY ],
xy2 : [ toX, toY ],
options : options || {},
overEls : overEls,
interval : me.dragDelay,
callbackDelay : me.afterActionDelay,
pathBatchSize : me.pathBatchSize,
onVoidOverEls : function () {
return overEls = []
},
onPointerEnter : function (el, options) {
},
onPointerLeave : function (el, options) {
},
onPointerOver : function (el, options) {
},
onPointerOut : function (el, options) {
},
onPointerMove : function (el, options) {
touch.clientX = options.clientX
touch.clientY = options.clientY
touch.pageX = me.viewportXtoPageX(options.clientX)
touch.pageY = me.viewportYtoPageY(options.clientY)
touch.currentEl = el
me.simulateTouchEventGeneric(el, 'move', options, { touchId : touchId })
}
})
},
// never used yet, should be called when touchMove goes out of the document
touchCancel : function (touchId, options) {
var touch = this.activeTouches[ touchId ]
if (!touch) throw "Can't find active touch: " + touchId
this.simulateTouchEventGeneric(touch.currentEl || touch.target, 'cancel', options, { touchId : touchId })
},
simulateTouchEvent : function (target, type, options, simOptions) {
options = options || {}
var global = this.global
var doc = global.document
var target = this.test.normalizeElement(target)
var clientX, clientY
if (("clientX" in options) && ("clientY" in options)) {
clientX = options.clientX
clientY = options.clientY
} else {
var center = this.test.findCenter(target);
clientX = center[ 0 ]
clientY = center[ 1 ]
}
var activeTouches = this.activeTouches
var touch = simOptions.touch
var touches = []
var targetTouches = []
for (var id in activeTouches) {
var currentTouch = activeTouches[ id ]
touches.push(currentTouch)
if (currentTouch.target == target) targetTouches.push(currentTouch)
}
var config = {
bubbles : true,
cancelable : true,
changedTouches : this.createTouchList([ touch ]),
touches : this.createTouchList(touches),
targetTouches : this.createTouchList(targetTouches),
altKey : options.altKey,
metaKey : options.metaKey,
ctrlKey : options.ctrlKey,
shiftKey : options.shiftKey
};
try {
var event = new global.TouchEvent(type, config)
} catch(e) {
// Legacy branch
var event = new global.CustomEvent(type, {
bubbles : true,
cancelable : true
})
Joose.O.extend(event, config);
}
target.dispatchEvent(event)
return event
},
createTouchList : function (touchList) {
var doc = this.global.document
if (doc.createTouch) {
var touches = [];
for (var i = 0; i < touchList.length; i++) {
var touchCfg = touchList[ i ];
touches.push(doc.createTouch(
doc.defaultView || doc.parentWindow,
touchCfg.target,
touchCfg.identifier || this.currentTouchId++,
touchCfg.pageX,
touchCfg.pageY,
touchCfg.screenX || touchCfg.pageX,
touchCfg.screenY || touchCfg.pageY,
touchCfg.clientX,
touchCfg.clientY
))
}
return doc.createTouchList.apply(doc, touches);
}
return touchList;
},
createTouch : function (target, clientX, clientY, identifier) {
var config = {
identifier : identifier || (this.currentTouchId++),
target : target,
clientX : clientX,
clientY : clientY,
screenX : 0,
screenY : 0,
// TODO should take scrolling into account
pageX : clientX,
pageY : clientY
};
if (this.global.Touch) {
return new this.global.Touch(config);
}
else {
return config;
}
},
buildTouchEventNamesMap : function () {
var supports = Siesta.Project.Browser.FeatureSupport().supports
var supportsPointerEnterLeaveEvents = !this.test.bowser.safari;
return {
pointer : {
start : ['pointerover'].concat(supportsPointerEnterLeaveEvents ? 'pointerenter' : []).concat('pointerdown'),
move : ['pointermove'],
end : ['pointerup', 'pointerout'].concat(supportsPointerEnterLeaveEvents ? 'pointerleave' : []),
cancel : ['pointercancel']
},
touch : {
start : 'touchstart',
move : 'touchmove',
end : 'touchend',
cancel : 'touchcancel'
}
};
},
simulateTouchEventGeneric : function (target, type, options, simOptions) {
simOptions = simOptions || {}
var me = this;
var target = me.test.normalizeElement(target)
var clientX, clientY
if (("clientX" in options) && ("clientY" in options)) {
clientX = options.clientX
clientY = options.clientY
}
else {
var center = me.test.findCenter(target);
clientX = center[ 0 ]
clientY = center[ 1 ]
}
var activeTouches = me.activeTouches
var touch
if (type === 'start') {
touch = me.createTouch(target, clientX, clientY)
activeTouches[ touch.identifier ] = touch
} else if (type === 'move') {
touch = me.createTouch(target, options.clientX, options.clientY, simOptions.touchId)
// "*move" events should be fired only from the "movePointerTemplate" method
// which provides the "clientX/clientY" properties
touch = activeTouches[ simOptions.touchId ] = touch;
} else if (type === 'end' || type === 'cancel') {
touch = activeTouches[ simOptions.touchId ]
target = touch.currentEl || touch.target
delete activeTouches[ simOptions.touchId ]
}
if (!touch) throw "Can't find active touch" + (simOptions.touchId ? ': ' + simOptions.touchId : '')
if (!simOptions.touchId) simOptions.touchId = touch.identifier
simOptions.touch = touch
var eventMap = this.getTouchEventNamesMap();
var supports = Siesta.Project.Browser.FeatureSupport().supports
var pointerEvent;
var touchEvent;
if (supports.PointerEvents && !(type === 'end' && me.isLongPressing())) {
eventMap.pointer[type].forEach(function (event) {
pointerEvent = me.simulateEvent(target, event, Object.assign({ pointerType : 'touch' }, options), simOptions)
});
}
// IE11 compat check
if (me.global.TouchEvent && (type !== 'end' || !this.isLongPressing())) {
touchEvent = me.simulateTouchEvent(target, eventMap.touch[type], options, simOptions);
}
// IE11 compat check
if (touchEvent && type === 'start') {
me.lastPointerDownPrevented = me.isEventPrevented(touchEvent);
}
if (type === 'end') {
me.possiblySimulateMouseEventsForTouchEnd(target, touch, options);
}
// IE11 compat check
return touchEvent || pointerEvent;
},
isLongPressing : function() {
return Date.now() - this.lastStartTouchTimeStamp > this.getLongPressDelay();
},
// fires mouse events (done by the browser after native touch events)
possiblySimulateMouseEventsForTouchEnd : function(target, touch, options) {
var me = this;
var didMove = me.lastStartTouch.clientX !== touch.clientX ||
me.lastStartTouch.clientY !== touch.clientY;
if (!didMove) {
var sequence;
var startWasPrevented = this.isEventPrevented(this.lastStartTouchEvent);
// Don't fire mouse events if this is a move pointer move ("touchDrag") operation,
// Chrome actually fires mouse events also when you move a little, probably since touch
// by its nature is not exact typically.
if (this.isLongPressing()) {
if (startWasPrevented) {
sequence = postLongPressSequenceWithTouchStartPrevented;
}
else {
sequence = postLongPressSequence;
}
} else if (!startWasPrevented) {
sequence = this.lastStartTouchWasOnNewTarget ? postTapSequence : postTapSequence.filter(function(event) {
return event !== 'mouseover';
});
}
sequence && sequence.forEach(function (event) {
me.simulateEvent(target, event, options)
});
}
}
}
});
})();
</pre>
</body>
</html>