/* Siesta 5.6.1 Copyright(c) 2009-2022 Bryntum AB https://bryntum.com/contact https://bryntum.com/products/siesta/license */ /** @class Siesta.Test.UserAgent.Mouse This is a mixin, providing the mouse events simulation functionality. */ Role('Siesta.Test.UserAgent.Mouse', { requires : [ //'simulateEvent', 'getSimulateEventsWith', 'normalizeElement', 'isTextInput' ], has: { // backward-compat only - just reference to a `this.simulator.currentPosition` currentPosition : null, /** * @cfg {Boolean} moveCursorBetweenPoints True to move the mouse cursor between for example two clicks on * separate elements (for better visual experience) */ moveCursorBetweenPoints : true, enableUnreachableClickWarning : true, autoScrollElementsIntoView : true, delayAfterScrollIntoView : 200 }, methods: { // not used anymore? previously been used in RootCause, as "instance" mouse move setCursorPosition : function (x, y, callback) { this.doMouseMove(this.simulator.currentPosition, [ x, y ], callback, null, null, { moveKind : 'instant' }); }, moveMouseAlongPath : function () { return this.moveCursorAlongPath.apply(this, arguments); }, /** * This method sequentially moves a cursor through a series of "path points". * * A path point can be one of the following: * * - An object `{ target : 'query', offset : [ x, y ] }`, where 'query' is any valid * {@link Siesta.Test.ActionTarget} query. This object will specify an absolute point on the page. * - A more compact notation with single letter properties `{ t : 'query', o : [ x, y ] }` * - An object `{ target : [ x, y ] }`, where 'x' and 'y' specifies an aboslute point on the page. * - An object `{ by : [ dx, dy ] }`, where `by` is an array, with a relative delta from previous point. * - An array with 3 elements: `[ 'query', x, y ]`, specifying an absolute point on the page, using * 'query' and 'x' 'y' offset. * - An array with 1 element: `[ [ x, y ] ]`, which is in turn an array, specifying an absolute * point on the page, using 'x' and 'y' page coordinates. * - An array with 2 elements: `[ dx, dy ]`, specifying a relative delta from previous point * * This method is generally intended to be used by {@link Siesta.Recorder.Recorder} with enabled * {@link Siesta.Recorder.Recorder#recordMouseMovePath recordMouseMovePath} option. That option can generate * a substantial amount of data, thus there is a focus on having a compact notation for it. * * @param {Array[path point]} pathArray An array of "path points" * @param {Function} callback A function to call after visiting all points * * @return {Promise} Returns a promise resolved once the action has completed */ moveCursorAlongPath : function (pathArray, callback) { var me = this var queue = new Siesta.Util.Queue({ deferer : this.originalSetTimeout, deferClearer : this.originalClearTimeout, interval : 0, processor : function (data) { var pathPoint = data.pathPoint var target, offset, isDelta if (me.typeOf(pathPoint) == 'Array') { if (me.typeOf(pathPoint[ 0 ]) == 'Array') { target = pathPoint[ 0 ] } else if (me.typeOf(pathPoint[ 0 ]) == 'String') { target = pathPoint[ 0 ] offset = pathPoint.length == 3 ? [ pathPoint[ 1 ], pathPoint[ 2 ] ] : null } else { isDelta = true target = pathPoint } } else { if (pathPoint.by) { isDelta = true target = pathPoint.by } else { target = pathPoint.t || pathPoint.target offset = pathPoint.o || pathPoint.offset } } if (isDelta) me.moveMouseBy(target, data.next, me) else me.moveMouseTo(target, data.next, me, offset) } }) Joose.A.each(pathArray, function (pathPoint) { queue.addStep({ isAsync : true, pathPoint : pathPoint }) }) return new Promise(function (resolve, _reject) { queue.run(function () { me.processCallbackFromTest(callback, null, me) resolve() }) }) }, /** * This method will simulate a mouse move to an xy-coordinate or an element (the center of it) * * @param {Siesta.Test.ActionTarget} target Target point to move the mouse to. * @param {Function} [callback] To run this method async, provide a callback method to be called after the operation is completed. * @param {Object} [scope] the scope for the callback * @param {Array} [offset] An X,Y offset relative to the target. Example: [20, 20] for 20px or ["50%", "50%"] to click in the center. * @param {Object} [waitForTarget] *private* True to wait for the target to exist before moving mouse * @param {Object} [options] Any options to use for the simulated DOM event * * @return {Promise} Returns a promise resolved once the action has completed */ moveMouseTo : function (target, callback, scope, offset, waitForTarget, options) { if (!target) throw new Error('Trying to call `moveMouseTo` without a target') // TODO this method should also accept an options object, so user can for example hold CTRL key during mouse operation if (waitForTarget !== false) { return this.waitForTargetAndSyncMousePosition(target, offset, function () { callback && callback.call(scope || this); }, [], false, undefined, options); } else { var me = this return new Promise(function (resolve, _reject) { // skip warning about clicking in an unreachable point of the element at me step // when mouse position is not yet updated // potentially the element will become reachable when the mouse is moved to the required point var data = me.getNormalizedTopElementInfo(target, true, 'moveMouseTo', offset); if (data) { me.syncCursor(data.globalXY, function () { callback && callback.call(scope || me); resolve() }, options); } else { // No point in continuing callback && callback.call(scope || me); resolve() } }) } }, /** * Alias for moveMouseTo, this method will simulate a mouse move to an xy-coordinate or an element (the center of it) * * @param {Siesta.Test.ActionTarget} target Target point to move the mouse to. * @param {Function} [callback] To run this method async, provide a callback method to be called after the operation is completed. * @param {Object} [scope] the scope for the callback * @param {Array} [offset] An X,Y offset relative to the target. Example: [20, 20] for 20px or ["50%", "50%"] to click in the center. * * @return {Promise} Returns a promise resolved once the action has completed */ moveCursorTo : function (target, callback, scope, offset) { return this.moveMouseTo.apply(this, arguments); }, /** * This method will simulate a mouse move by an x a y delta amount * @param {Array} delta The delta x and y distance to move, e.g. [20, 20] for 20px down/right, or [0, 10] for just 10px down. * @param {Function} [callback] To run this method async, provide a callback method to be called after the operation is completed. * @param {Object} [scope] the scope for the callback * @param {Object} [options] Any options to use for the simulated DOM events * * @return {Promise} Returns a promise resolved once the action has completed */ moveMouseBy : function (delta, callback, scope, options) { return this.moveCursorBy.apply(this, arguments); }, /** * This method will simulate a mouse move by an x and y delta amount * @param {Array} delta The delta x and y distance to move, e.g. [20, 20] for 20px down/right, or [0, -10] for 10px up. * @param {Function} [callback] To run this method async, provide a callback method to be called after the operation is completed. * @param {Object} [scope] the scope for the callback * @param {Object} [options] Any options to use for the simulated DOM events * * @return {Promise} Returns a promise resolved once the action has completed */ moveCursorBy : function (delta, callback, scope, options) { if (!delta) { throw 'Trying to call moveCursorBy without relative distances'; } // Normalize target var target = [ this.simulator.currentPosition[ 0 ] + delta[ 0 ], this.simulator.currentPosition[ 1 ] + delta[ 1 ] ]; return this.doMouseMove(this.simulator.currentPosition, target, callback, scope, undefined, options); }, // private, to be removed in the future? // we don't support source `xy` coordinates in native simulator doMouseMove : function (xy, xy2, callback, scope, params, options) { var promise = this.simulator.simulateMouseMove(xy2[ 0 ], xy2[ 1 ], options, params) return this.runPromiseAsync(promise, 'mousemove', callback, scope) }, // candidate for removal, only used in 2 places, feels redundant with "doMouseMouse" syncCursor : function (toXY, callback, options) { var fromXY = this.simulator.currentPosition; if (toXY[ 0 ] !== fromXY[ 0 ] || toXY[ 1 ] !== fromXY[ 1 ]) { this.doMouseMove(fromXY, toXY, callback, this, undefined, options); } else // already aligned callback && callback(); }, runPromiseAsync : function (promise, actionDesc, callback, callbackScope, errback) { var me = this var async = this.beginAsync(null, function () { me.fail(me.formatString("Action `{actionDesc}` failed to complete within {time}ms", { actionDesc : actionDesc, time : this.defaultTimeout })) return true }) return promise.then(function (result) { me.endAsync(async) me.processCallbackFromTest(callback, result !== undefined ? [ result ] : [], callbackScope || me) }, function (exception) { me.endAsync(async) if (errback) errback(exception) else if (!me.isFinished()) { me.fail("Command failed: " + exception) me.processCallbackFromTest(callback, [], callbackScope || me) } }) }, // This method is called before mouse interactions (the "method" param, along with its "args") to assure that target is visible and reachable. // It also handles cases where the target is moved or made unreachable while the cursor is moving towards it. // In such unusual cases, a wait is added and the method calls itself to start over waitForTargetAndSyncMousePosition : function (target, offset, method, args, waitForTargetTop, syncMousePosition, options) { var originalXY var targetIsNotAPoint = this.typeOf(target) != 'Array' var oldSuppressPassedWaitForAssertion = this.suppressPassedWaitForAssertion this.suppressPassedWaitForAssertion = true var me = this return new Promise(function (resolve, reject) { me.chain( { waitForAnimations : [] }, // Initial wait for target to be // 1. in the dom, // 2. visible targetIsNotAPoint && { waitForElementVisible : target }, me.autoScrollElementsIntoView ? function (next) { if (me.scrollTargetIntoView(target, offset) === true) // me "waitFor" has been added because of Ext6 behaviour (see https://www.assembla.com/spaces/bryntum/tickets/2211#/activity/ticket:) // Ext6 listens to scroll event on grid view and sets the "pointer-event : none" on the grid view el in the handler // Problem happens during click. // Seems, depending from browser engine, "scroll" event may be fired after slight delay, already after the "mousedown" // even has been fired, then, with "pointer-events" none on grid view, grid container becomes a top element // and further `mouseup` and `click` happens on it, instead of original element // the "pointer-event" style is reset back in the another ExtJS handler, which is buffered for 100ms // so we need to wait > 100ms, waiting for 200ms // potential race condition me.waitFor(me.delayAfterScrollIntoView, next) else next() } : null, function (next) { var data = me.getNormalizedTopElementInfo(target, true, method.toString(), offset) // No target available, possibly page is reloading. Go back to waiting for something to appear if (!data) { me.waitForTarget(target, function () { var data = me.getNormalizedTopElementInfo(target, true, method.toString(), offset) originalXY = data.globalXY if (me.moveCursorBetweenPoints && syncMousePosition !== false) { me.syncCursor(originalXY, next, options) } else { next() } }) return } originalXY = data.globalXY; if (me.moveCursorBetweenPoints && syncMousePosition !== false) { me.syncCursor(originalXY, next, options) } else { next() } }, // After moving cursor, we again wait as something might have changed in the while we moved the cursor waitForTargetTop !== false && targetIsNotAPoint && function (next) { me.waitForTarget(target, next, me, null, offset) }, function (_next) { var data = me.getNormalizedTopElementInfo(target, true, method.toString(), offset) var newXY = data && data.globalXY // If target has moved or disappeared, start over after a short wait if (targetIsNotAPoint && (!data || originalXY[0] !== newXY[0] || originalXY[1] !== newXY[1])) { me.diag(Siesta.Resource('Siesta.Test.Browser','targetMoved')) me.waitFor(100, function() { me.waitForTargetAndSyncMousePosition(target, offset, method, args, waitForTargetTop, syncMousePosition).then(resolve, reject) }); } else { me.suppressPassedWaitForAssertion = oldSuppressPassedWaitForAssertion; // Here we're done - call original method if its a function method && method.apply && method.apply(me, args) resolve() } } ) // eof chain }) // eof promise }, getCursorPagePosition : function () { return [ this.viewportXtoPageX(this.simulator.currentPosition[ 0 ]), this.viewportYtoPageY(this.simulator.currentPosition[ 1 ]) ] }, // el - the target // callback // scope // options for the events emitted // actionName (string) - the method of simulator to call (will return promise) // offset // performTargetCheck, true to waitFor target appearing - false to avoid genericMouseAction : function (el, callback, scope, options, actionName, offset, waitForTargetAndSyncMousePosition) { var me = this if (this.typeOf(el) == 'Function') { scope = callback callback = el el = null } waitForTargetAndSyncMousePosition = waitForTargetAndSyncMousePosition && Boolean(el); el = el || this.getCursorPagePosition() var targetCheckPromise = waitForTargetAndSyncMousePosition !== false ? this.waitForTargetAndSyncMousePosition(el, offset, actionName, undefined, undefined, undefined, options) : Promise.resolve() return me.runPromiseAsync(targetCheckPromise.then(function () { options = options ? Joose.O.copy(options) : {} // skip warning about clicking in an unreachable point of the element at me step // when mouse position is not yet updated // potentially the element will become reachable when the mouse is moved to the required point var data = me.getNormalizedTopElementInfo(el, true, actionName, offset) if (!data) { // No point in continuing return } // marking data as preliminary, indicating that it should be updated before the click data.originalEl = el data.method = actionName data.offset = offset options.clientX = options.clientX != null ? options.clientX : data.localXY[ 0 ] options.clientY = options.clientY != null ? options.clientY : data.localXY[ 1 ] options.testUniqueId = me.uniqueId return me.simulator[ actionName ](data, options); }), 'generic click', callback, scope) }, /** * This method will simulate a mouse click in the center of the specified DOM element, * or at current cursor position if no target is provided. * * Note, that it will first calculate the central point of the specified element and then * will pick the top-most DOM element from that point. For example, if you will provide a grid row as the `el`, * then click will happen on top of the central cell, and then will bubble to the row itself. * In most cases this is the desired behavior. * * Example: * * t.click(t.getFirstRow(grid), function () { ... }) * * The 1st argument for this method can be omitted. In this case, Siesta will use the current cursor position: * * t.click(function () { ... }) * * This method returns a `Promise` which is resolved once the click has completed: * * t.click('#someEl').then(function () { * return t.click('#anotherEl') * }).then(function () { * return t.click('#yetAnotherEl') * }) * * See also {@link Siesta.Test#chain chain} method for slimer chaining notation. * * @param {Siesta.Test.ActionTarget} [el] One of the {@link Siesta.Test.ActionTarget} values to convert to DOM element * @param {Function} [callback] A function to call after the click * @param {Object} [scope] The scope for the callback * @param {Object} [options] Any options to use for the simulated DOM event * @param {Array} [offset] An X,Y offset relative to the target. Example: [20, 20] for 20px or ["50%", "100%-2"] * to click in the center horizontally and 2px from the bottom edge. * @return {Promise} Returns a promise resolved once the click has completed */ click : function (el, callback, scope, options, offset, waitForTarget) { return this.genericMouseAction(el, callback, scope, options, 'simulateMouseClick', offset, waitForTarget) }, /** * This method will simulate a mouse right click in the center of the specified DOM/Ext element, * or at current cursor position if no target is provided. * * Note, that it will first calculate the centeral point of the specified element and then * will pick the top-most DOM element from that point. For example, if you will provide a grid row as the `el`, * then click will happen on top of the central cell, and then will bubble to the row itself. * In most cases this is the desired behavior. * * Example: * * t.rightClick(t.getFirstRow(grid), function () { ... }) * * The 1st argument for this method can be omitted. In this case, Siesta will use the current cursor position: * * t.rightClick(function () { ... }) * * This method returns a `Promise` which is resolved once the right click has completed * * @param {Siesta.Test.ActionTarget} [el] One of the {@link Siesta.Test.ActionTarget} values to convert to DOM element * @param {Function} [callback] A function to call after click. * @param {Object} [scope] The scope for the callback * @param {Object} [options] Any options to use for the simulated DOM event * @param {Array} [offset] An X,Y offset relative to the target. Example: [20, 20] for 20px or ["50%", "100%-2"] * to click in the center horizontally and 2px from the bottom edge. * @return {Promise} Returns a promise resolved once the right click has completed */ rightClick : function (el, callback, scope, options, offset, waitForTarget) { return this.genericMouseAction(el, callback, scope, options, 'simulateRightClick', offset, waitForTarget) }, /** * This method will simulate a mouse double click in the center of the specified DOM/Ext element, * or at current cursor position if no target is provided. * * Note, that it will first calculate the centeral point of the specified element and then * will pick the top-most DOM element from that point. For example, if you will provide a grid row as the `el`, * then click will happen on top of the central cell, and then will bubble to the row itself. * In most cases this is the desired behavior. * * Example: * * t.doubleClick(t.getFirstRow(grid), function () { ... }) * * The 1st argument for this method can be omitted. In this case, Siesta will use the current cursor position: * * t.doubleClick(function () { ... }) * * This method returns a `Promise` which is resolved once the double click has completed * * @param {Siesta.Test.ActionTarget} [el] One of the {@link Siesta.Test.ActionTarget} values to convert to DOM element * @param {Function} [callback] A function to call after click. * @param {Object} [scope] The scope for the callback * @param {Object} [options] Any options to use for the simulated DOM event * @param {Array} [offset] An X,Y offset relative to the target. Example: [20, 20] for 20px or ["50%", "100%-2"] * to click in the center horizontally and 2px from the bottom edge. * @return {Promise} Returns a promise resolved once the double click has completed */ doubleClick : function (el, callback, scope, options, offset, waitForTarget) { return this.genericMouseAction(el, callback, scope, options, 'simulateDoubleClick', offset, waitForTarget) }, /** * This method will simulate a mousedown event in the center of the specified DOM element, * or at current cursor position if no target is provided. * * @param {Siesta.Test.ActionTarget} el * @param {Object} options any extra options used to configure the DOM event * @param {Array} [offset] An X,Y offset relative to the target. Example: [20, 20] for 20px or ["50%", "100%-2"] * to click in the center horizontally and 2px from the bottom edge. * @param {Function} [callback] A function to call after mousedown. * @param {Object} [scope] The scope for the callback * @return {Promise} Returns a promise resolved once the action has completed */ mouseDown : function (el, options, offset, callback, scope, performTargetCheck) { return this.genericMouseAction(el, callback, scope, options, 'simulateMouseDown', offset, performTargetCheck); }, /** * This method will simulate a mousedown event in the center of the specified DOM element, * or at current cursor position if no target is provided. * * @param {Siesta.Test.ActionTarget} el * @param {Object} options any extra options used to configure the DOM event * @param {Array} [offset] An X,Y offset relative to the target. Example: [20, 20] for 20px or ["50%", "100%-2"] * to click in the center horizontally and 2px from the bottom edge. * @return {Promise} Returns a promise resolved once the action has completed */ mouseUp : function (el, options, offset, callback, scope) { return this.genericMouseAction(el, callback, scope, options, 'simulateMouseUp', offset, true); }, /** * This method will simulate a drag and drop operation between either two points or two DOM elements. * * @param {Siesta.Test.ActionTarget} source {@link Siesta.Test.ActionTarget} value for the drag starting point * @param {Siesta.Test.ActionTarget} target {@link Siesta.Test.ActionTarget} value for the drag end point * @param {Function} [callback] To run this method async, provide a callback method to be called after the drag operation is completed. * @param {Object} [scope] the scope for the callback * @param {Object} options any extra options used to configure the DOM event * @param {Boolean} dragOnly true to skip the mouseup and not finish the drop operation. * @param {Array} [sourceOffset] An X,Y offset relative to the source. Example: [20, 20] for 20px or ["50%", "100%-2"] to click in the center horizontally and 2px from the bottom edge. * @param {Array} [targetOffset] An X,Y offset relative to the target. Example: [20, 20] for 20px or ["50%", "100%-2"] to click in the center horizontally and 2px from the bottom edge. * @return {Promise} Returns a promise resolved once the action has completed */ dragTo : function (source, target, callback, scope, options, dragOnly, sourceOffset, targetOffset) { var me = this; if (!target) throw new Error('No drag target defined'); source = source || me.getCursorPagePosition() return new Promise(function (resolve, reject) { me.chain( { mouseDown : source, offset : sourceOffset, options : options }, function (next) { var data = me.getNormalizedTopElementInfo(target, true, 'dragTo', targetOffset); target = data.globalXY; next(); }, { moveCursorTo : function () { return target }, options : options }, dragOnly ? null : { mouseUp : null, options : options }, function () { me.processCallbackFromTest(callback, null, scope || me); resolve() } ); }) }, /** * This method will simulate a drag and drop operation from a point (or DOM element) and move by a delta. * * @param {Siesta.Test.ActionTarget} source {@link Siesta.Test.ActionTarget} value as the drag starting point * @param {Array} delta The amount to drag from the source coordinate, expressed as [x,y]. E.g. [50, 10] will drag 50px to the right and 10px down. * @param {Function} [callback] To run this method async, provide a callback method to be called after the drag operation is completed. * @param {Object} [scope] the scope for the callback * @param {Object} options any extra options used to configure the DOM event * @param {Boolean} dragOnly true to skip the mouseup and not finish the drop operation. * @param {Array} [offset] An X,Y offset relative to the target. Example: [20, 20] for 20px or ["50%", "100%-2"] to click in the center horizontally and 2px from the bottom edge. * @return {Promise} Returns a promise resolved once the action has completed */ dragBy : function (source, delta, callback, scope, options, dragOnly, offset) { var me = this; if (!delta) throw new Error('No drag delta defined'); source = source || this.getCursorPagePosition() return new Promise(function (resolve, reject) { me.chain( { mouseDown : source, offset : offset, options : options }, { moveCursorBy : delta, options : options }, dragOnly ? null : { mouseUp : null, options : options }, function () { me.processCallbackFromTest(callback, null, scope || me); resolve() } ); }) }, /** * This method will simulate a wheel event on the specified DOM element. * * @param {Siesta.Test.ActionTarget} [el] One of the {@link Siesta.Test.ActionTarget} values to convert to a DOM element * @param {Function} [callback] A function to call after the action. * @param {Object} [scope] The scope for the callback * @param {Object} [options] Any options to use for the simulated DOM event * @param {Object} options any extra options used to configure the DOM event * @param {Object} options.deltaX a double representing the horizontal scroll amount. * @param {Object} options.deltaY a double representing the vertical scroll amount. Not supported in native events simulation. * @param {Object} options.deltaZ a double representing scroll amount for the z-axis. Not supported in native events simulation. * @param {Array} [offset] An X,Y offset relative to the target. Example: [20, 20] for 20px or ["50%", "100%-2"] to click in the center horizontally and 2px from the bottom edge. * @return {Promise} Returns a promise resolved once the action has completed */ wheel : function (el, callback, scope, options, offset, waitForTarget) { return this.genericMouseAction(el, callback, scope, options, 'simulateMouseWheel', offset, waitForTarget) } } });