/*

Siesta 5.1.0
Copyright(c) 2009-2018 Bryntum AB
https://bryntum.com/contact
https://bryntum.com/products/siesta/license

*/
Role('Siesta.Test.Simulate.Mouse', {

    requires        : [
        'simulateEvent', 'getSimulateEventsWith', '$'
    ],

    does : [
        Siesta.Util.Role.Dom,
        Siesta.Util.Role.CanCalculatePageScroll,
        Siesta.Test.Browser.Role.CanGetElementFromPoint
    ],

    has: {
        // Current viewport coordinates of the cursor
        // this will be a shared array instance between all subtests
        // it should not be overwritten, instead modify individual elements:
        // NO: this.currentPosition = [ 1, 2 ]
        // YES: this.currentPosition[ 0 ] = 1
        // YES: this.currentPosition[ 1 ] = 2
        currentPosition         : {
            init : function () { return [ 0, 0 ]; }
        },

        /**
         * @cfg {Int} dragDelay The delay between individual drag events (mousemove)
         */
        dragDelay                       : 25,

        pathBatchSize                   : bowser.msie ? 10 : 5,

        mouseMovePrecision              : 1,

        enableUnreachableClickWarning   : true,

        overEls                         : Joose.I.Array,
        lastMouseOverEl                 : null,

        mouseState                      : 'up',

        // The last element we fired 'mousedown' upon
        lastMouseDownEl                 : null,

        pointerEventNamesMap            : {
            lazy : function () {
                if (window.PointerEvent)
                    return {
                        pointerdown         : 'pointerdown',
                        pointerup           : 'pointerup',
                        pointerover         : 'pointerover',
                        pointerout          : 'pointerout',
                        pointerenter        : 'pointerenter',
                        pointerleave        : 'pointerleave',
                        pointermove         : 'pointermove'
                    }
                else
                    if (window.MSPointerEvent)
                        return {
                            pointerdown         : 'MSPointerDown',
                            pointerup           : 'MSPointerUp',
                            pointerover         : 'MSPointerOver',
                            pointerout          : 'MSPointerOut',
                            pointerenter        : 'MSPointerEnter',
                            pointerleave        : 'MSPointerLeave',
                            pointermove         : 'MSPointerMove'
                        }
                    else
                        return {}
            }
        }
    },


    after : {
        cleanup : function () {
            this.overEls            = null
            this.lastMouseDownEl    = null
            this.lastMouseOverEl    = null
        }
    },


    override : {

        normalizeEventName : function (eventName) {
            var eventMap        = this.getPointerEventNamesMap()

            return eventMap[ eventName ] || this.SUPERARG(arguments);
        },


        simulateEvent : function (el, eventName) {
            var supportsPointerEvents   = Siesta.Project.Browser.FeatureSupport().supports.PointerEventsGeneric

            if (supportsPointerEvents ? /pointerdown$/i.test(eventName) : eventName == 'mousedown') {
                this.mouseState   = 'down'

                this.lastMouseDownEl = el;
            }

            if (supportsPointerEvents ? /pointerup$/i.test(eventName) : eventName == 'mouseup') {
                this.mouseState   = 'up'

                this.lastMouseUpEl = el;
            }

            var event   = this.SUPERARG(arguments)

            if (/pointerdown$/i.test(eventName)) {
                this.lastPointerDownPrevented = this.isEventPrevented(event);
            }

            // in FF for 'textInput' events the returning value can be `undefined`
            if (this.test.mouseVisualizer && event) this.test.mouseVisualizer.onEventSimulated(event, this.currentPosition)

            return event
        }
    },


    methods: {
        // private
        createMouseEvent: function (type, options, el) {
            var global      = this.global
            var doc         = el.ownerDocument
            var event

            var isPointer   = type.match(/^(ms)?pointer/i)

            options         = this.prepareMouseEventOptions(type, options, el)

            if (!bowser.msie && global.MouseEvent) {
                if (type === 'wheel') {
                    event           = new global.WheelEvent(type, options);
                } else if (isPointer) {
                    // this is non IE branch, so no mess with MS prefix

                    // Chrome sets button to -1 for pointermove
                    if (type === 'pointermove') {
                        options.button = -1;
                    }

                    event           = new global.PointerEvent(type, options);
                } else {
                    event           = new global.MouseEvent(type, options);
                }
            }
            // use W3C standard when available and allowed by "simulateEventsWith" option
            else if (doc.createEvent && this.getSimulateEventsWith() == 'dispatchEvent') {

                if (type === 'wheel') {
                    // https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/ff975847(v=vs.85)
                    /*
                    * A space-separated list of any of the following values:
                        Alt
                        The left or right Alt key is pressed.

                        AltGraph
                        The Ctrl and Alt keys are pressed.

                        CapsLock
                        The Caps Lock toggle is enabled.

                        Control
                        The left or right Ctrl key is pressed.

                        Meta
                        The Meta/Control key is pressed.

                        NumLock
                        The Num Lock toggle is enabled.

                        Scroll
                        The Scroll Lock toggle is enabled.

                        Shift
                        The left or right Shift key is pressed.

                        Win
                        The left or right Windows logo key is pressed.
                    **/
                    var modifiersArg = '';

                    if (options.ctrlKey) modifiersArg = 'Control';
                    if (options.altKey) modifiersArg += ' Alt';
                    if (options.shiftKey) modifiersArg += ' Shift';

                    event           = doc.createEvent('WheelEvent');

                    event.initWheelEvent(
                        type,
                        options.bubbles,
                        options.cancelable,
                        options.view,
                        options.detail,
                        options.screenX,
                        options.screenY,
                        options.clientX,
                        options.clientY,
                        options.button,
                        options.relatedTarget || doc.documentElement,
                        modifiersArg,
                        options.deltaX || 0,
                        options.deltaY || 0,
                        options.deltaZ || 0,
                        options.deltaMode || 0);
                } else {
                    event           = doc.createEvent(isPointer ? (isPointer[ 1 ] ? 'MS' : '') + 'PointerEvent' : 'MouseEvents');

                    event[ isPointer ? 'initPointerEvent' : 'initMouseEvent' ](
                        type, options.bubbles, options.cancelable, options.view, options.detail,
                        options.screenX, options.screenY, options.clientX, options.clientY,
                        options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
                        options.button, options.relatedTarget || doc.documentElement,
                        // the following extra args are used in the "initPointerEvent"
                        // offsetX, offsetY
                        null, null,
                        // width, height
                        null, null,
                        // pressure, rotation
                        null, null,
                        // tiltX, tiltY
                        null, null,
                        // pointerId
                        options.pointerId,
                        // pointerType
                        // NOTE: this has to be set to "mouse" (IE11) or 4 (IE10, 11) because otherwise
                        // ExtJS5 blocks the event
                        // need to investigate what happens in SenchaTouch
                        options.pointerType,
                        // timestamp
                        null,
                        // isPrimary
                        null
                    );
                }

            } else if (doc.createEventObject) {
                event       = doc.createEventObject();

                $.extend(event, options);

                event.button = { 0: 1, 1: 4, 2: 2 }[ event.button ] || event.button;
            }

            // in Edge, the "pageX/pageY" properties of the event object are calculated by browser completely
            // wrong - need to override those
            if (this.bowser.msedge) {
                global.Object.defineProperty(event, 'pageX', { value : options.pageX })
                global.Object.defineProperty(event, 'pageY', { value : options.pageY })
            }

            // Mouse over is used in some certain edge cases which interfer with this tracking
            if (!/(mouse|pointer)over$/.test(type) && !/(mouse|pointer)out$/.test(type)) {
                var elWindow    = doc.defaultView || doc.parentWindow;
                var cursorX     = options.clientX;
                var cursorY     = options.clientY;

                // Potentially we're interacting with an element inside a nested frame, which means the coordinates are local to that frame
                if (elWindow !== global) {
                    var offsets = this.$(elWindow.frameElement).offset();

                    cursorX     += offsets.left;
                    cursorY     += offsets.top;
                }

                if (!options.doNotUpdateCurrentPosition) {
                    // TODO should be moved to `simulateEvent` (and set right before the `dispatchEvent` call)
                    this.currentPosition[ 0 ]   = cursorX;
                    this.currentPosition[ 1 ]   = cursorY;
                }
            }

            return event;
        },


        prepareMouseEventOptions : function (type, options, el) {
            var global      = this.global

            options         = $.extend({
                bubbles     : !/(ms)?(mouse|pointer)enter/i.test(type) && !/(ms)?(mouse|pointer)leave/i.test(type),
                cancelable  : !/(ms)?(mouse|pointer)move/i.test(type),
                view        : global,
                detail      : 0,

                screenX     : 0,
                screenY     : 0,

                ctrlKey     : false,
                altKey      : false,
                shiftKey    : false,
                metaKey     : false,

                /*
                 * https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
                 *
                 * A number representing a given button:

                 0: Main button pressed, usually the left button or the un-initialized state
                 1: Auxiliary button pressed, usually the wheel button or the middle button (if present)
                 2: Secondary button pressed, usually the right button
                 3: Fourth button, typically the Browser Back button
                 4: Fifth button, typically the Browser Forward button

                 * */
                button          : 0,
                /*
                 * https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
                 *
                 * 0  : No button or un-initialized
                 * 1  : Left button
                 * 2  : Right button
                 * 4  : Wheel button or middle button
                 * 8  : 4th button (typically the "Browser Back" button)
                 * 16 : 5th button (typically the "Browser Forward" button)
                 *
                 * */
                buttons         : 0,
                relatedTarget   : undefined,

                // pointerType
                // NOTE: this has to be set to "mouse" (IE11) or 4 (IE10, 11) because otherwise
                // ExtJS5 blocks the event
                // need to investigate what happens in SenchaTouch
                pointerType     : bowser.msie ? 4 : 'mouse'

            }, options);

            if (!("clientX" in options) || !("clientY" in options)) {
                var center          = this.test.findCenter(el);

                options.clientX     = center[ 0 ]
                options.clientY     = center[ 1 ]
            }

            options.clientX         = Math.round(options.clientX)
            options.clientY         = Math.round(options.clientY)

            // edge seems to incorrectly calculate pageX/pageY, providing explicitly
            if (this.bowser.msedge && (!("pageX" in options) || !("pageY" in options))) {
                options.pageX       = this.viewportXtoPageX(options.clientX)
                options.pageY       = this.viewportYtoPageY(options.clientY)
            }

            // Not supported in IE
            if ("screenX" in window) {
                options.screenX     = Math.round(global.screenX + options.clientX)
                options.screenY     = Math.round(global.screenY + options.clientY)
            }

            return options
        },


        simulateMouseMove : function (x, y, options, params) {
            var me              = this

            params              = params || {}

            var pathBatchSize   = params.pathBatchSize
            var async           = params.async
            var mouseMovePrecision = params.mouseMovePrecision

            if (params.moveKind == 'instant') {
                pathBatchSize           = 10000
                mouseMovePrecision      = 10000
            }

            return this.movePointerTemplate({
                xy              : this.currentPosition,
                xy2             : [ x, y ],
                options         : options,

                overEls         : this.overEls,
                interval        : async !== false ? this.dragDelay : 0,
                callbackDelay   : async !== false ? 50 : 0,
                pathBatchSize   : pathBatchSize || me.pathBatchSize,
                mouseMovePrecision : mouseMovePrecision || me.mouseMovePrecision,

                onVoidOverEls   : function () {
                    return me.overEls  = []
                },

                onPointerEnter  : function (el, options) {
                    me.onPointerEnter(el, options)
                },

                onPointerLeave  : function (el, options) {
                    me.onPointerLeave(el, options)
                },

                onPointerOver   : function (el, options) {
                    me.onPointerOver(el, options)
                },

                onPointerOut    : function (el, options) {
                    me.onPointerOut(el, options)
                },

                onPointerMove   : function (el, options) {
                    me.onPointerMove(el, options)
                }
            })
        },

        // xy, xy2, overEls, pathBatchSize, interval, callbackDelay, options,
        // onPointerEnter, onPointerLeave, onPointerOver, onPointerOut, onPointerMove
        movePointerTemplate: function (args) {
            var document    = this.global.document,
                me          = this,
                overEls     = args.overEls,
                // Remember last visited element, since a previous action may have changed the DOM
                // which possibly should trigger a mouseout event
                lastOverEl  = overEls[ overEls.length - 1 ];

            if (lastOverEl && this.nodeIsUnloaded(lastOverEl)) {
                lastOverEl  = null
                overEls     = args.onVoidOverEls()
            }

            // this method works as follows:
            // `path` - contains in array of points
            // we split that array into batches with size - `pathBatchSize`, but, each batch can't be less than `mouseMovePrecision`
            // every batch is processed by one queue step (see below), so for every batch there's one call to `processor`
            // inside the processor, there's a loop, which iterates the batch with the delta, equal to `mouseMovePrecision`,
            // but no less than 1st and last point

            // always simulate drag with 1px precision
            var mouseMovePrecision  = me.mouseState == 'down' ? 1 : args.mouseMovePrecision || me.mouseMovePrecision
            var pathBatchSize       = Math.max(args.pathBatchSize, mouseMovePrecision)
            var options             = args.options || {}
            var supports            = Siesta.Project.Browser.FeatureSupport().supports

            var path        = this.getPathBetweenPoints(args.xy, args.xy2);

            var queue       = new Siesta.Util.Queue({
                deferer         : this.test.originalSetTimeout,
                deferClearer    : this.test.originalClearTimeout,

                interval        : args.interval,
                callbackDelay   : args.callbackDelay,

                observeTest     : this.test,

                processor       : function (data, index) {
                    var fromIndex   = data.sourceIndex,
                        toIndex     = data.targetIndex;

                    // replace 0 with 1 to avoid infinite loop
                    var delta       = Math.min(toIndex - fromIndex, mouseMovePrecision) || 1

                    for (var j = fromIndex; j <= toIndex; j += delta) {
                        var point       = path[ j ];
                        var info        = me.elementFromPoint(point[ 0 ], point[ 1 ], false, null, true);

                        // targetEl will possibly be from the nested iframe
                        // and `localXY in `info` will contain local viewport point for `x, y` in that iframe
                        var targetEl    = info.el

                        // Might get null if moving over a non-initialized frame (seen in Chrome)
                        if (!targetEl) continue

                        var x           = info.localXY[ 0 ]
                        var y           = info.localXY[ 1 ]

                        if (targetEl !== lastOverEl) {
                            me.onElementAtCursorChanged(targetEl, lastOverEl, x, y, options);
                            lastOverEl = targetEl;
                        }

                        args.onPointerMove(targetEl, $.extend({ clientX : x, clientY : y }, options), j < toIndex)
                    }

                    // check again if the PointerMove simulation triggered a change of the element at the cursor
                    // and process it if needed
                    if (point) {
                        info        = me.elementFromPoint(point[ 0 ], point[ 1 ], false, null, true);

                        if (info.el && info.el !== lastOverEl) {
                            me.onElementAtCursorChanged(info.el, lastOverEl, x, y, options);
                        }
                    }
                    // eof for
                }
            });

            var pathLength = path.length

            if (pathLength <= pathBatchSize && mouseMovePrecision >= pathBatchSize) {
                // special case, when only the 1st and last points of the path will simulate mouse events
                // in this case, we want to simulate the events for *two* initial and *two* final points
                // so that in the begining and at the end of the path simulation is more accurate
                queue.addStep({
                    sourceIndex : 0,
                    targetIndex : Math.min(1, pathLength - 1)
                });

                if (pathLength >= 3)
                    queue.addStep({
                        sourceIndex : pathLength - 2,
                        targetIndex : pathLength - 1
                    })
            } else
                for (var i = 0, l = pathLength; i < l; i += pathBatchSize) {
                    queue.addStep({
                        sourceIndex : i,
                        targetIndex : Math.min(i + pathBatchSize - 1, pathLength - 1)
                    });
                }

            queue.addStep({
                processor : function () {
                    me.afterMouseInteraction()
                }
            });

            return new Promise(function (resolve, reject) {
                queue.run(resolve)
            })
        },


        onPointerEnter : function (el, options) {
            var me                    = this;
            var supportsPointerEvents = Siesta.Project.Browser.FeatureSupport().supports.PointerEventsGeneric

            if (supportsPointerEvents) me.simulateEvent(el, "pointerenter", options)
            me.simulateEvent(el, "mouseenter", options)
        },


        onPointerLeave : function (el, options) {
            var me                    = this;
            var supportsPointerEvents = Siesta.Project.Browser.FeatureSupport().supports.PointerEventsGeneric

            if (supportsPointerEvents) me.simulateEvent(el, "pointerleave", options)
            me.simulateEvent(el, "mouseleave", options)
        },


        onPointerOver : function (el, options) {
            var me                    = this;
            var supportsPointerEvents = Siesta.Project.Browser.FeatureSupport().supports.PointerEventsGeneric

            if (supportsPointerEvents) me.simulateEvent(el, "pointerover", options)
            me.simulateEvent(el, "mouseover", options)
        },


        onPointerOut : function (el, options) {
            var me                    = this;
            var supportsPointerEvents = Siesta.Project.Browser.FeatureSupport().supports.PointerEventsGeneric

            if (supportsPointerEvents) me.simulateEvent(el, "pointerout", options)
            me.simulateEvent(el, "mouseout", options)
        },


        onPointerMove : function (el, options) {
            var me                    = this;
            var supportsPointerEvents = Siesta.Project.Browser.FeatureSupport().supports.PointerEventsGeneric

            options.buttons = me.mouseState == 'up' ? 0 : 1

            if (supportsPointerEvents) me.simulateEvent(el, "pointermove", options)
            me.simulateEvent(el, "mousemove", options)
        },


        onElementAtCursorChanged : function (targetEl, lastOverEl, clientX, clientY, options) {
            var me       = this,
                supports = Siesta.Project.Browser.FeatureSupport().supports,
                overEls  = me.overEls;

            if (lastOverEl && !me.nodeIsOrphan(lastOverEl) && !me.nodeIsUnloaded(lastOverEl)) {
                me.onPointerOut(lastOverEl, $.extend({
                    clientX       : clientX,
                    clientY       : clientY,
                    relatedTarget : targetEl
                }, options))
            }

            for (var i = overEls.length - 1; i >= 0; i--) {
                var el = overEls[ i ];

                if (me.nodeIsUnloaded(el) || me.nodeIsOrphan(el))
                    overEls.splice(i, 1);
                else if (el !== targetEl && me.$(el).has(targetEl).length === 0) {
                    if (supports.mouseEnterLeave) {
                        me.onPointerLeave(el, $.extend({
                            clientX       : clientX,
                            clientY       : clientY,
                            relatedTarget : targetEl
                        }, options))
                    }
                    overEls.splice(i, 1);
                }
            }

            // "mouseover" should be simulated before "mouseleave"
            me.onPointerOver(targetEl, $.extend({
                clientX       : clientX,
                clientY       : clientY,
                relatedTarget : lastOverEl
            }, options))

            if (supports.mouseEnterLeave && jQuery.inArray(targetEl, overEls) == -1) {
                var els          = []
                var docEl        = targetEl.ownerDocument.documentElement
                var mouseEnterEl = targetEl

                // collecting all the els for which to fire the "mouseenter" event, strictly speaking these can be any elements
                // (because of absolute positioning) but in most cases it will be just parent elements
                while (mouseEnterEl && mouseEnterEl != docEl) {
                    els.unshift(mouseEnterEl)
                    mouseEnterEl = mouseEnterEl.parentNode
                }

                for (var i = 0; i < els.length; i++) {
                    if (jQuery.inArray(els[ i ], overEls) == -1) {
                        me.onPointerEnter(els[ i ], $.extend({
                            clientX       : clientX,
                            clientY       : clientY,
                            relatedTarget : lastOverEl
                        }, options))

                        overEls.push(els[ i ]);
                    }
                }
            }
        },


        // Check if the mouse interaction triggered a DOM update causing the last interacted element to be removed from the DOM
        // In this case we should simulate a new 'mouseover' event on whatever appeared under the cursor.
        afterMouseInteraction : function() {
            // var overEls         = this.overEls,
            //     lastOverEl      = overEls[ overEls.length - 1 ]
            //
            // //URL might have changed, then ignore
            // if (!this.global.document.body) return;
            //
            // if (lastOverEl&&this.nodeIsUnloaded(lastOverEl)) {
            //     lastOverEl  = null
            //     this.overEls = []
            //
            //     //after page reload we want to simulate the `mouseover`
            //     //for the element appeared at the current cursor position
            //     this.mouseOver(this.currentPosition);
            // }
        },


        simulateMouseDown: function (clickInfo, options) {
            var supportsPointerEvents   = Siesta.Project.Browser.FeatureSupport().supports.PointerEventsGeneric

            return this.processMouseActionSteps(
                clickInfo,
                options,
                [
                    supportsPointerEvents ?
                        { event : "pointerdown", interval : 0 }
                    :
                        null,
                    { event : "mousedown", focus : true }
                ]
            );
        },


        simulateMouseUp: function (clickInfo, options) {
            var el                    = clickInfo.el;
            // Should only happen if parent el of the mousedown/up events are the same
            var targetChanged         = this.lastMouseDownEl && el !== this.lastMouseDownEl && !($.contains(el, this.lastMouseDownEl) || $.contains(this.lastMouseDownEl, el));
            var shouldFireClick       = !targetChanged || !(this.bowser.safari || this.bowser.gecko);
            var supportsPointerEvents = Siesta.Project.Browser.FeatureSupport().supports.PointerEventsGeneric

            return this.processMouseActionSteps(
                clickInfo,
                options,
                [
                    supportsPointerEvents ?
                        { event : "pointerup", interval : 0 }
                    :
                        null,
                    { event : "mouseup" }
                ].concat(shouldFireClick ?
                    [
                        { event : "click" }
                    ] :
                    []
                )
            );
        },

        // private, should not be used in tests
        mouseOver: function (el, options) {
            var info        = this.test.getNormalizedTopElementInfo(el, true);

            if (!info) return;

            options         = options || {}

            options.clientX = options.clientX != null ? options.clientX : info.localXY[0];
            options.clientY = options.clientY != null ? options.clientY : info.localXY[1];

            var supportsPointerEvents   = Siesta.Project.Browser.FeatureSupport().supports.PointerEventsGeneric

            if (supportsPointerEvents) this.simulateEvent(el, 'pointerover', options);
            this.simulateEvent(el, 'mouseover', options);
        },


        // private, should not be used in tests
        mouseOut: function (el, options) {
            var info        = this.test.getNormalizedTopElementInfo(el, true);

            if (!info) return;

            options         = options || {}

            options.clientX = options.clientX != null ? options.clientX : info.localXY[0];
            options.clientY = options.clientY != null ? options.clientY : info.localXY[1];

            var supportsPointerEvents   = Siesta.Project.Browser.FeatureSupport().supports.PointerEventsGeneric

            if (supportsPointerEvents) this.simulateEvent(el, 'pointerout', options);
            this.simulateEvent(el, 'mouseout', options);
        },


        processMouseActionSteps : function (clickInfo, options, steps) {
            // trying to get the top element again, enabling the warning if needed
            // do it here and not in the `genericMouseAction` method to allow scenario
            // when target element appears when mouse moves to the click point
            if (clickInfo.originalEl && this.enableUnreachableClickWarning) {
                this.test.getNormalizedTopElementInfo(clickInfo.originalEl, false, clickInfo.method, clickInfo.offset)
            }

            var me          = this

            var x           = clickInfo.globalXY[ 0 ]
            var y           = clickInfo.globalXY[ 1 ]
            var isOption    = clickInfo.el.nodeName.toLowerCase() === 'option';

            var doc         = me.global.document

            var prevScrollTop   = this.getPageScrollY()

            // re-evaluate the target el - it might have changed while we were syncing the cursor position
            var target       = isOption ? clickInfo.el : me.elementFromPoint(x, y, false, clickInfo.el)
            var targetParent = target.parentNode;

            var targetHasChanged    = false

            var queue       = new Siesta.Util.Queue({
                deferer         : this.test.originalSetTimeout,
                deferClearer    : this.test.originalClearTimeout,

                interval        : 10,
                callbackDelay   : me.afterActionDelay,

                observeTest     : this.test,

                processor       : function (data) {
                    if (me.lastPointerDownPrevented && /mouse/i.test(data.event)) return

                    // XXX this has to be investigated more deeply (notably the <body> vs <html> scrolling, etc)
                    // - When simulating events browser performs weird scrolls on the document.
                    // Seems it tries to make the point of simulated event visible on the screen.
                    // This is native browser behavior out of our control.
                    // Thing is, when the document is scrolled, `elementFromPoint` returns different
                    // element for the same point. Because of that the logic for clicks is vulnerable.
                    // Scenario is - "mousedown" (or may be "mouseup") is simulated, scroll position changes
                    // further "click" event happens on different element

                    // body can be absent if the doubleclick happens on the anchor and page is reloaded in the middle
                    // of double click
                    var delta       = doc.body ? me.getPageScrollY() - prevScrollTop : 0
                    var elAtCursor  = isOption ? target : me.elementFromPoint(x, y - delta, false, target)
                    var fireEl      = elAtCursor;

                    if (!isOption && data.recaptureTarget) { target = elAtCursor; targetHasChanged = false }

                    // The "click" event should be canceled if "mousedown/up" happened on different elements,
                    // _unless_ these elements has parent/child relationship
                    if (!isOption && elAtCursor !== target && !($.contains(elAtCursor, target) || $.contains(target, elAtCursor))) targetHasChanged = true

                    // Special treatment of click event firing:
                    // * Don't fire click if a node was moved in the DOM tree, or if was orphaned
                    // * Chrome + IE fires click on the common ancestor of mousedown target + mouseup target after drag drop
                    if (!isOption && data.event === 'click') {
                        var nodeMovedInDomTree = elAtCursor === target && elAtCursor.parentNode !== targetParent;

                        // Don't fire click if a node was moved in the DOM tree, or if target or mouseDown target was orphaned
                        // or if mouseDownElement !== mouseUpElement
                        if (nodeMovedInDomTree || me.nodeIsOrphan(target) || (me.lastMouseDownEl && me.nodeIsOrphan(me.lastMouseDownEl))) return;

                        var mouseDownUpTargetsChanged;

                        // Check if mousedownElement differs from what is at cursor
                        if (me.lastMouseDownEl &&elAtCursor !== me.lastMouseDownEl &&
                            !($.contains(elAtCursor, me.lastMouseDownEl) || $.contains(me.lastMouseDownEl, elAtCursor))) {
                            mouseDownUpTargetsChanged = true;
                            fireEl = me.getCommonAncestor(elAtCursor, me.lastMouseDownEl);

                            // Also check if mouseupElement differs from what is at cursor
                        } else if (me.lastMouseUpEl && me.lastMouseUpEl !== elAtCursor &&
                            !($.contains(elAtCursor, me.lastMouseUpEl))) {

                            mouseDownUpTargetsChanged = true;
                            fireEl = me.getCommonAncestor(elAtCursor, me.lastMouseUpEl);
                        }

                        // When target changed, Chrome + IE fires click on the common ancestor after drag drop
                        if (mouseDownUpTargetsChanged) {
                            if (me.bowser.gecko || me.bowser.safari) {
                                // Safari + FF does not fire click on the common ancestor after drag drop
                                return;
                            } else {
                                // mouseDown/mouseUp happened in 2 different frames?
                                if (!fireEl) return;
                            }
                        }
                    }

                    if (targetHasChanged && data.cancelIfTargetChanged) {
                        return;
                    }

                    var event       = me.simulateEvent(fireEl, data.event, options);

                    if (!me.lastPointerDownPrevented && data.focus) {
                        me.mimicFocusOnMouseDown(elAtCursor, event);
                    }

                    if (!isOption) {
                        // Check if this event triggered another element to be visible at cursor, if so handle pointerleave/mouseleave.
                        var elementAtPoint = me.elementFromPoint(x, y - delta, false, target);

                        if (elementAtPoint !== elAtCursor) {
                            me.onElementAtCursorChanged(elementAtPoint, elAtCursor, x, y - delta, options);
                        }
                    }
                }
            })

            Joose.A.each(steps, function (step) {
                step && queue.addStep(step)
            })

            return new Promise(function (resolve, reject) {
                queue.run(function () {
                    me.afterMouseInteraction();

                    resolve()
                })
            })
        },


        // private
        simulateMouseClick: function (clickInfo, options) {
            var supportsPointerEvents   = Siesta.Project.Browser.FeatureSupport().supports.PointerEventsGeneric

            return this.processMouseActionSteps(
                clickInfo,
                options,
                [
                    supportsPointerEvents ?
                        { event : "pointerdown", interval : 0 }
                    :
                        null,
                    { event : "mousedown", focus : true },

                    supportsPointerEvents ?
                        { event : "pointerup", interval : 0 }
                    :
                        null,
                    { event : "mouseup", interval : 0 },

                    { event : "click", cancelIfTargetChanged : true }
                ]
            )
        },

        // private
        simulateRightClick: function (clickInfo, options) {
            // Mac doesn't fire mouseup when right clicking
            var isMac       = navigator.platform.indexOf('Mac') > -1;

            var supportsPointerEvents   = Siesta.Project.Browser.FeatureSupport().supports.PointerEventsGeneric

            options         = options || {};
            options.button  = options.buttons  = 2;

            return this.processMouseActionSteps(
                clickInfo,
                options,
                [
                    supportsPointerEvents ?
                        { event : "pointerdown", interval : 0 }
                    :
                        null,
                    { event : "mousedown", focus : true }
                ].concat(isMac ? [] :
                    [
                        supportsPointerEvents ?
                            { event : "pointerup", interval : 0 }
                        :
                            null,
                        { event : "mouseup", interval : 0 }
                    ]
                ).concat(
                    [ { event : "contextmenu" } ]
                )
            )
        },

        // private
        simulateDoubleClick: function (clickInfo, options) {
            var supportsPointerEvents   = Siesta.Project.Browser.FeatureSupport().supports.PointerEventsGeneric

            return this.processMouseActionSteps(
                clickInfo,
                options,
                [
                    supportsPointerEvents ?
                        { event : "pointerdown", interval : 0 }
                    :
                        null,
                    { event : "mousedown", focus : true },

                    supportsPointerEvents ?
                        { event : "pointerup", interval : 0 }
                        :
                        null,
                    { event : "mouseup", interval : 0 },

                    { event : "click", cancelIfTargetChanged : true },

                    supportsPointerEvents ?
                        { event : "pointerdown", interval : 0 }
                        :
                        null,
                    { event : "mousedown", recaptureTarget : true, focus : true },

                    supportsPointerEvents ?
                        { event : "pointerup", interval : 0 }
                        :
                        null,
                    { event : "mouseup", interval : 0 },

                    { event : "click" , cancelIfTargetChanged : true, interval : 0 },
                    { event : "dblclick" , cancelIfTargetChanged : true }
                ]
            )
        },


        // private
        mimicFocusOnMouseDown : function (el, mouseDownEvent) {
            // only do focus if `mousedown` event is not prevented by outside world
            if (this.isEventPrevented(mouseDownEvent)) return;

            var test        = this.test

            // if we've clicked text input element just do regular focus
            if (test.isElementFocusable(el)) {
                test.focus(el, true)
                return
            }

            var doc         = el.ownerDocument
            var win         = doc.defaultView || doc.parentWindow
            var body        = doc.body

            if (!body) return;

            // otherwise focus the nearest parent with non-null `tabIndex` attribute
            // as an edge case an "<html> element can be clicked
            while (el && el != body && el != doc) {
                // IE-specific: don't look up the parent nodes when clicked an element with "unselectable" attribute set to "on"
                // and do not focus the body
                // "unselectable" attr should not be used to determine focusability state
                if (bowser.msie && el.getAttribute('unselectable') == 'on') return

                if (test.isElementFocusable(el)) {
                    test.focus(el, true)
                    return
                }

                el          = el.parentNode
            }

            // focus body as the last resort to trigger the "blur" event on the currently focused element
            test.focus(body || doc.documentElement, true)
        },


        simulateMouseWheel : function (targetInfo, options) {
            var eventName   = 'wheel';
            var doc         = targetInfo.el.ownerDocument;

            // For legacy browsers where we don't use dispatchEvent, fallback to 'mousewheel' event ('wheel' cannot be simulated with doc.createEventObject in <= IE9)
            if (!doc.createEvent || this.getSimulateEventsWith() !== 'dispatchEvent') {
                eventName   = 'mousewheel';
            }

            return this.processMouseActionSteps(
                targetInfo,
                options,
                [
                    { event : eventName }
                ]
            );
        },


        // private
        getPathBetweenPoints: function (from, to) {
            if (
                typeof from[0] !== 'number' ||
                typeof from[1] !== 'number' ||
                typeof to[0] !== 'number'   ||
                typeof to[1] !== 'number'   ||
                isNaN(from[0])              ||
                isNaN(from[1])              ||
                isNaN(to[0])                ||
                isNaN(to[1])
            ) {
                throw new Error('Incorrect arguments passed to getPathBetweenPoints: ' + from + ', ' + to);
            }

            var stops = [],
                x0 = Math.floor(from[0]),
                x1 = Math.floor(to[0]),
                y0 = Math.floor(from[1]),
                y1 = Math.floor(to[1]),
                dx = Math.abs(x1 - x0),
                dy = Math.abs(y1 - y0),
                sx, sy, err, e2;

            if (x0 < x1) {
                sx = 1;
            } else {
                sx = -1;
            }

            if (y0 < y1) {
                sy = 1;
            } else {
                sy = -1;
            }
            err = dx - dy;

            while (x0 !== x1 || y0 !== y1) {
                e2 = 2 * err;
                if (e2 > -dy) {
                    err = err - dy;
                    x0 = x0 + sx;
                }

                if (e2 < dx) {
                    err = err + dx;
                    y0 = y0 + sy;
                }
                stops.push([x0, y0]);
            }

            var last = stops[stops.length-1];

            if (stops.length > 0 && (last[0] !== to[0] || last[1] !== to[1])) {
                // the points of the path can be modified in the move mouse method - thus pushing a copy
                // of the original target
                stops.push(to.slice());
            }
            return stops;
        }
    }
});