UNPKG

siesta-lite

Version:

Stress-free JavaScript unit testing and functional testing tool, works in NodeJS and browsers

639 lines (480 loc) 23.3 kB
/* 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) }); } } } }); })();