    /*

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

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

        does        : [ JooseX.Observable ],

        requires : [
            '$',
            'typeOf',
            'createTextEvent',
            'createMouseEvent',
            'createKeyboardEvent'
        ],

        has : {
            bowser              : function () { return bowser },
            browser             : function () { return bowser },

            actionDelay         : 100,
            afterActionDelay    : 100,

            /**
             * @cfg {String} simulateEventsWith
             *
             * This option is IE9-strict mode (and probably above) specific. It specifies, which events simulation function Siesta should use.
             * The choice is between 'dispatchEvent' (W3C standard) and 'fireEvent' (MS interface) - both are available in IE9 strict mode
             * and both activates different event listeners. See this blog post for detailed explanations:
             * <http://www.digitalenginesoftware.com/blog/archives/76-DOM-Event-Model-Compatibility-or-Why-fireEvent-Doesnt-Trigger-addEventListener.html>
             *
             * Valid values are "dispatchEvent" and "fireEvent".
             *
             * The framework specific adapters choose the most appropriate value automatically (unless explicitly configured).
             */
            simulateEventsWith  : {
                is   : 'rw',
                lazy : function () { return 'dispatchEvent' }
            }
        },

        methods : {

            normalizeEventName : function (eventName) {
                return eventName
            },

            /**
             * This method will simulate an event triggered by the passed element. If no coordinates are supplied in the options object, the center of the element
             * will be used.
             * @param {Siesta.Test.ActionTarget} el
             * @param {String} type The type of event (e.g. 'mouseover', 'click', 'keypress')
             * @param {Object} the options for the event. See http://developer.mozilla.org/en/DOM/event for reference.
             */
            simulateEvent : function (el, type, options) {
                var global  = this.global;
                options     = options || {};

                if (this.typeOf(el) == 'Array') {
                    if (el.length == 0) el = this.currentPosition.slice()

                    if (!('clientX' in options)) {
                        options.clientX = el[ 0 ];
                    }

                    if (!('clientY' in options)) {
                        options.clientY = el[ 1 ];
                    }
                }

                el          = this.test.normalizeElement(el);
                type        = this.normalizeEventName(type)

                var evt     = this.createEvent(type, options, el);

                if (evt) {
                    this.maintainScrollPositionDuring(function () {
                        evt.synthetic = true;

                        this.mimicBrowserBehaviorBefore(evt, type, el);

                        this.dispatchEvent(el, type, evt);

                        !this.isEventPrevented(evt) && this.mimicBrowserBehaviorAfter(evt, type, el);
                    })
                }

                this.fireEvent('eventsimulated', evt, el, type, options)

                return evt;
            },

            createEvent : function (type, options, el) {
                var event

                if (/^textinput$/i.test(type)) {
                    event = this.createTextEvent(type, options, el);
                } else if (/^mouse(over|out|down|up|move|enter|leave)|contextmenu|wheel|(dbl)?click$/.test(type) || /^(ms)?pointer/i.test(type)) {
                    event = this.createMouseEvent(type, options, el);
                } else if (/^key(up|down|press)$/.test(type)) {
                    event = this.createKeyboardEvent(type, options, el);
                } /*else if (/^touch/.test(type)) {
                 return this.createTouchEvent(type, options, el);
                 }*/
                else if (/^change$|^input$|^submit/.test(type)) {
                    event = this.createGenericEvent(type, options, el);
                }
                else
                    event = this.createHtmlEvent(type, options, el);

                // IE>=9 somehow reports that "defaultPrevented" property of the event object is `false`
                // even that "preventDefault()" has been called on the object
                // more over, immediately after call to "preventDefault()" the property is updated
                // but down in stack it is replaced with "false" again somehow
                // we setup our own, additional property, indicating that event has been prevented
                if (event && bowser.msie && bowser.version >= 9) {
                    var prev = event.preventDefault

                    event.preventDefault = function () {
                        arguments.callee.$prevented = true;
                        this.returnValue = false

                        return prev && prev.apply(this, arguments)
                    }
                }

                return event
            },

            isEventPrevented : function (event) {
                // our custom property - takes highest priority
                if (event.preventDefault && this.typeOf(event.preventDefault.$prevented) == 'Boolean') return event.preventDefault.$prevented

                // W3C standards property
                if (this.typeOf(event.defaultPrevented) == 'Boolean') return event.defaultPrevented

                return event.returnValue === false
            },

            createGenericEvent : function (type, options, el) {
                var doc                 = el.ownerDocument;

                if (doc.createEvent && this.getSimulateEventsWith() == 'dispatchEvent') {
                    var evt             = doc.createEvent("Events");

                    evt.initEvent(type, true, true);

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

                    event.srcElement    = el

                    event.bubbles       = options.bubbles
                    event.cancelBubble  = !options.bubbles
                    event.type          = type

                    return event
                }
            },

            createHtmlEvent : function (type, options, el) {
                var doc = el.ownerDocument;

                if (doc.createEvent && this.getSimulateEventsWith() == 'dispatchEvent') {
                    var evt = doc.createEvent("HTMLEvents");
                    evt.initEvent(type, false, false);
                    return evt;
                } else if (doc.createEventObject) {
                    return doc.createEventObject();
                }
            },

            dispatchEvent : function (el, type, evt) {

                // use W3C standard when available and allowed by "simulateEventsWith" option
                if (el.dispatchEvent && this.getSimulateEventsWith() == 'dispatchEvent') {
                    el.dispatchEvent(evt);
                } else if (el.fireEvent) {
                    // IE 6,7,8 can't dispatch many events cleanly - throws exceptions
                    try {
                        // this is the serios nominant to the best-IE-bug-ever prize and it's IE7 specific
                        // accessing the "scrollLeft" property on document or body triggers a synchronous(!) "resize" event on the window
                        // ExtJS uses a singleton for Ext.EventObj and its "target" property gets overwritten with "null"
                        // thus consequent event handlers fails
                        // doing an access to that property to cache it
                        var doc  = this.global.document.documentElement;
                        var body = this.global.document.body;

                        var xxx = doc && doc.scrollLeft || body && body.scrollLeft || 0;

                        el.fireEvent('on' + type.toLowerCase(), evt);
                    } catch (e) {
                    }

                    // in IE, the "fireEvent" does not bubble the "change" event
                    // we try to bubble it manually (to fix the TaskBoard2.x "subtasks" tests, but then
                    // the target el is not set correctly and it goes too deep into Ext sources, so commenting for now
    //                if (type == 'change') {
    //                    if (el != doc && el != body && el.parentElement) this.dispatchEvent(el.parentElement, type, evt)
    //                }
                } else
                    throw "Can't dispatch event: " + type

                return evt;
            },

            mimicBrowserBehaviorBefore : function (event, type, target) {
            },

            mimicBrowserBehaviorAfter : function (event, type, target) {
                var tagName = target.tagName.toLowerCase();

                switch (type) {
                    case 'mousedown':
                        // it seems selection is actually cleared in "pointerup" (mouseup)
                        if (this.isTextInput(target)) {
                            this.mimicClearTextSelection(target);
                        }
                        break;
                    case 'click':
                        if (
                            bowser.msie && bowser.version < 11 &&
                            tagName === 'a' &&
                            target.getAttribute("href")
                        ) {
                            this.mimicHashUpdate(target);
                        } else if (tagName === 'option' && !target.getAttribute('disabled')) {
                            this.mimicOptionSelect(target);
                        }
                        break;
                    case 'dblclick':
                        if (this.isTextInput(target)) {
                            this.mimicDblClickTextSelection(target);
                        }
                        break;
                    case 'keydown':
                        var KeyCodes = Siesta.Test.UserAgent.KeyCodes().keys

                        if (event.keyCode === KeyCodes.BACKSPACE) {
                            this.mimicHistoryChangeAfterBackspace(event, target);
                        }
                        break;
                }
            },

            // IE9+
            // Breaks IE9 with Ext JS 5.1.0, tested in .540_extjs_type.t.js?5.1.0
            mimicClearTextSelection : function (target) {
                var extVersion = this.global.Ext && this.global.Ext.versions;
                var isExtJS51  = extVersion && extVersion.extjs && extVersion.extjs.equals('5.1.0.107');

                if (!bowser.msie || !isExtJS51 || (bowser.version > 9)) {
                    this.test.selectText(target, target.value.length - 1, target.value.length);

                    this.test.setCaretPosition(target, target.value.length);
                }
            },

            mimicDblClickTextSelection : function (target) {
                this.test.selectText(target);
            },

            // After a click action in old IE, change location hash manually
            mimicHashUpdate : function (el) {
                var href = el.getAttribute("href").match(/#(.*)/);

                if (href) {
                    this.global.location.hash = href[1];
                }
            },

            mimicHistoryChangeAfterBackspace : function (event, target) {
                // Chrome+Safari doesn't trigger page navigation (as of Chrome52)
                if (bowser.webkit || bowser.blink) return;

                var doc      = target.ownerDocument;

                if (!target.isContentEditable && doc.designMode.toLowerCase() !== "on" && !this.isTextInput(target)) {
                    var elWindow = target.ownerDocument.defaultView || target.ownerDocument.parentWindow;

                    if (event.shiftKey) {
                        this.mimicNextHistory(elWindow);
                    } else {
                        this.mimicPreviousHistory(elWindow);
                    }
                }
            },

            mimicPreviousHistory : function (global) {
                global.history.back();
            },

            mimicNextHistory : function (global) {
                global.history.forward();
            },

            mimicOptionSelect : function (optionNode) {
                var select   = this.$(optionNode).closest('select')[0];
                var oldValue = select.value;

                select.value = optionNode.value;

                if (oldValue !== optionNode.value) {
                    this.simulateEvent(select, "change");
                }
            }
        }
    });