    /*

    Siesta 5.3.0
    Copyright(c) 2009-2019 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 && 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;
            }
        }
    });