ST.context.Local = ST.define({ extend: ST.context.Base, flushingMessages: false, isLocalContext: true, constructor: function (config) { var me = this, pointerElement = me.pointerElement = document.createElement('div'), className = 'orion-mouse-pointer'; ST.context.Local.superclass.constructor.call(me, config); if (ST.isMac || ST.isiOS) { className += ' orion-mouse-pointer-mac'; } ST.apply(me, config); me.futureData = {}; pointerElement.className = className; if (ST.event.Injector) { me.injector = new ST.event.Injector({ context: me, translate: me.eventTranslation }); me.gestureIndicators = []; me.touchCount = 0; me.lastGestureEndTime = 0; me.setupGesturePublisher(); } ST.inBrowser = true; }, cleanup: function () { var me = this; if (me.visualFeedback) { ST.each(me.gestureIndicators, function (indicator) { me.hideGestureIndicator(indicator); }); } }, stop: function (resolve, reject) { var me = this; if (me.visualFeedback) { ST.defer(function () { me.hidePointer(); if (resolve) { resolve(); } }, 1000); } else { if (resolve) { resolve(); } } }, attachComponentIfAvailable: function () { this.cmp = this.el.getComponent(); }, gestureStartEvents: { mousedown: 1, pointerdown: 1, touchstart: 1 }, gestureEndEvents: { mouseup: 1, pointerup: 1, touchend: 1, pointercancel: 1, touchcancel: 1 }, // called before adding the event to the player queue // for Local we make a Playable // for WebDriver we don't... since we make the Playable only in the target initEvent: function (event) { ST.logger.debug('Local.initEvent(',event,')'); event.context = this; if (!event.isPlayable) { var playable = this.createPlayable(event); ST.logger.debug('Local.initEvent() returning playable=',playable); return playable; } else { return event; } }, callFn: function (event, done) { return event.fn.call(event, done); }, ready: function (event, resolve, reject) { try { var ready = event.ready(); if (ready) { resolve(ready); } else { reject(); } } catch (e) { reject(e); } }, inject: function (event, resolve, reject) { var me = this, type = event.type, targetEl = event.targetEl, relatedTargetEl = event.relatedEl; // console.log('ST.context.Local.play(',playable); if (!me.injector) { throw new Error('Injector events are not supported in a webdriver remote target'); } if (type === 'tap') { me.expandTap(event); } else if (type === 'gridcolumntrigger') { me.expandOverAction(event); } else if (type === 'menuitemover') { me.expandOverAction(event); } else if (type === 'type') { me.expandType(event); } else if (type === 'scroll') { me.expandScroll(event); } else if (type === 'draganddrop') { me.expandDragAndDrop(event); } else if (type === 'dblclick' && ST.isModern) { me.expandDblClick(event); } else if (event.type && event.type !== 'wait') { me.injector.injectEvent(event, targetEl, relatedTargetEl); if (me.gestureStartEvents[type]) { me.touchCount++; if (me.visualFeedback) { me.showGestureIndicator(); } } else if (me.gestureEndEvents[type]) { me.touchCount--; if (me.visualFeedback) { me.hideGestureIndicator(); } me.lastGestureEndTime = +new Date(); } else if (me.visualFeedback && type === 'click') { if (((+new Date()) - me.lastGestureEndTime) > 300) { // just in case we are playing a 'click' with no preceding mousedown me.showGestureIndicator(); me.hideGestureIndicator(); // will hide once show animation completes } } else if (type === 'keydown') { me.hidePointer(); } } event.state = 'done'; resolve(); }, expandDragAndDrop: function (event) { var ddEvents = [], dragTarget = event.args.dragTarget || event.dragTarget, dragX = event.args.dragX || event.dragX, dragY = event.args.dragY || event.dragY, dropTarget = event.args.dropTarget || event.dropTarget, dropX = event.args.dropX || event.dropX, dropY = event.args.dropY || event.dropY, dragStart = { type: 'pointerdown', target: dragTarget, delay: event.delay }; if (typeof dragX !== 'undefined') { dragStart.x = dragX; } if (typeof dragY !== 'undefined') { dragStart.y = dragY; } // if no dropTarget is defined, the drag target will be the target if (typeof dropTarget === 'undefined') { dropTarget = '@'; } ddEvents = [ dragStart, // we have to have a minimum of 1px of movement to preserve the drag action { type: 'pointermove', target: dropTarget, delay: event.delay, x: dropX + 1, y: dropY }, { type: 'pointermove', target: dropTarget, delay: event.delay, x: dropX, y: dropY }, { type: 'pointerup', target: dropTarget, delay: event.delay, x: dropX, y: dropY } ]; this.player.add(0, ddEvents); }, expandScroll: function (event) { var x = event.args.x || event.x, y = event.args.y || event.y, pos = event.args.pos || event.pos, el = event.targetEl && ST.fly(event.targetEl), cmp = el && el.getComponent(), scrollable, isScrollable; if (ST.isArray(pos) && pos.length === 2) { x = pos[0]; y = pos[1]; } if (cmp) { scrollable = cmp.getScrollable ? cmp.getScrollable() : null; if (scrollable) { // if we have a scrollable, use its scrollTo method; should be standard in 6+, so no modern check needed scrollable.scrollTo(x, y, false) } else if (cmp.scrollBy) { // otherwise, fall back to common scrollBy cmp.scrollBy(x, y, false); } } else { isScrollable = function (node) { if (node === null) { return null; } else if (node === document.body) { return node; } else if (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth) { return node; } else { return isScrollable(node.parentNode); } }; if (event.targetEl) { scrollable = isScrollable(event.targetEl.dom); } // the scrollable will either be the passed-in target, or the next scrollable ancestor if (scrollable) { scrollable.scrollTop = y; scrollable.scrollLeft = x; } } }, expandTap: function (event) { var x = event.args.x || event.x, y = event.args.y || event.y, options = { metaKey: event.metaKey, shiftKey: event.shiftKey, ctrlKey: event.ctrlKey, button: event.args.button || event.button, detail: event.detail }, queue = ST.gestureQueue, tapEvents = [], target = event.target, x,y, el, cmp; if (typeof event.args.x === 'undefined') { x = event.x; } else { x = event.args.x; } if (typeof event.args.y === 'undefined') { y = event.y; } else { y = event.args.y; } if (typeof target === 'function') { target = event.target(); } // TODO: Move special click rules for menuitem (and other affected components) to actual API classes el = event.targetEl && ST.fly(event.targetEl); cmp = el && el.getComponent(); // Injecting a "tap" on a OK/Cancel button can result in the button being // destroyed (see https://sencha.jira.com/browse/ORION-408). We use relative // indexing on the "target" to pull the target through from the initial event // so that we don't have to search for it (and fail) on subsequent events. // event.target for tap/click used to be a Playable but now it is a function... // so target: event.target won't work... better would be to use the result of event.target() if it's a function? // we'll not enforce visibility on the pointerup/click events; in some cases, the target will be hidden by the handlers for // for pointerdown tapEvents = [ { type: 'pointerdown', target: target, delay: event.delay, x: x, y: y }, { type: 'pointerup', target: -1, delay: 0, x: x, y: y, visible: null }, { type: 'click', target: -2, delay: 0, x: x, y: y, visible: null }, // if a gesture queue is set up, we need to add a wait in case the events are asynchronous // the wait() will poll for a limited time until expected event is announced { type: 'wait', target: -2, delay: 0, x: x, y: y, ready: this.isTapGestureReady } ]; // Add any modifer keys for (i = 0; i < tapEvents.length; i++) { ST.applyIf(tapEvents[i], options); } if (queue) { queue.activate(); } this.player.add(0, tapEvents); }, expandDblClick: function (event) { var x = event.args.x || event.x, y = event.args.y || event.y, options = { metaKey: event.metaKey, shiftKey: event.shiftKey, ctrlKey: event.ctrlKey, button: event.args.button || event.button, detail: event.detail }, queue = ST.gestureQueue, tapEvents = [], target = event.target, x, y, el, cmp; if (typeof event.args.x === 'undefined') { x = event.x; } else { x = event.args.x; } if (typeof event.args.y === 'undefined') { y = event.y; } else { y = event.args.y; } if (typeof target === 'function') { target = event.target(); } // TODO: Move special click rules for menuitem (and other affected components) to actual API classes el = event.targetEl && ST.fly(event.targetEl); cmp = el && el.getComponent(); // we'll not enforce double clicks by expanding it to two different clicks and removing delay for EXTjs modern tapEvents = [ { type: 'pointerdown', target: target, delay: 400, x: x, y: y }, { type: 'pointerup', target: -1, delay: 0, x: x, y: y, visible: null }, { type: 'click', target: -2, delay: 0, x: x, y: y, visible: null }, { type: 'pointerdown', target: target, delay: 0, x: x, y: y }, { type: 'pointerup', target: -1, delay: 0, x: x, y: y, visible: null }, { type: 'click', target: -2, delay: 0, x: x, y: y, visible: null }, // if a gesture queue is set up, we need to add a wait in case the events are asynchronous // the wait() will poll for a limited time until expected event is announced { type: 'wait', target: -2, delay: 0, x: x, y: y, ready: this.isTapGestureReady } ]; // Add any modifer keys for (i = 0; i < tapEvents.length; i++) { ST.applyIf(tapEvents[i], options); } if (queue) { queue.activate(); } this.player.add(0, tapEvents); }, expandOverAction: function (event) { var x = event.args.x || event.x, y = event.args.y || event.y, target = event.target, x, y, events; if (typeof event.args.x === 'undefined') { x = event.x; } else { x = event.args.x; } if (typeof event.args.y === 'undefined') { y = event.y; } else { y = event.args.y; } if (typeof target === 'function') { target = event.target(); } events = [ { type: 'focus', target: target, x: x, y: y, visible: null }, { type: 'mouseenter', target: target, x: x, y: y, visible: null }, { type: 'mouseover', target: target, x: x, y: y, visible: null } ]; this.player.add(0, events); }, isTapGestureReady: function () { var queue = ST.gestureQueue; if (!queue) { return true; } return queue.complete(this.target.id, 'tap'); }, onEnd: function (resolve) { var queue = ST.gestureQueue; if (queue) { queue.deactivate(); } if (resolve) { resolve(); } }, expandType: function (event) { var me = this, text = event.args.text || event.text, key = event.args.key || event.key, targetEl = event.targetEl, typingDelay = me.typingDelay, caret = event.args.caret || event.caret, target = event.future ? event.future.locator.target : event.target, cmp, el, fld, events, i, len; if (typeof event.target === 'string') { cmp = targetEl.getComponent(); if (cmp && cmp.el.dom === targetEl.dom) { // if using classic toolkit, we can use getFocusEl() if (ST.isClassic) { el = cmp.getFocusEl(); } // if not classic and the type is a textfield, we can retrieve the input from the component else if (cmp.isXType('textfield')) { if (cmp.getComponent) { fld = cmp.getComponent(); } else { // in 6.5, Ext.field.Text no longer extends Ext.field.Decorator, so getComponent() doesn't exist // however, the "field" component is just the component itself, so easy enough fld = cmp; } el = fld.input || fld.inputElement; // 6.2+ changed input to inputElement } // otherwise, just fallback to the el; this will accomodate Sencha Touch, and is the default for // what getFocusEl() returns in the modern toolkit else { el = cmp.el || cmp.element; } if (el) { targetEl = new ST.Element(el.dom); } } } if (text) { events = []; len = text.length; if (caret === undefined) { events = [{ type: "tap", target: target, args: {}, visibility: null }]; } for (i = 0; i < len; ++i) { var resetValue = false; key = text.charAt(i); if (i === 0 && caret === undefined) { resetValue = true; } events.push( { type: 'keydown', target: targetEl, key: key, delay: typingDelay, reset: resetValue },{ type: 'keyup', target: targetEl, key: key, delay: 0 } ); // console.log('ST.context.Local.expandType, i=',i,', events=',events); } } else if (key) { // special keys events = [ { type: 'keydown', target: targetEl, key: key },{ type: 'keyup', target: targetEl, key: key, delay: 0 } ]; } else { return; } events[0].delay = event.delay || me.eventDelay; if (caret != null) { events[0].caret = caret; } // console.log('expandType calling player.add(0 with events=',events); ST.player().add(0,events); }, onPointChanged: function(x, y) { var me = this, indicators = me.gestureIndicators, indicator; if (me.visualFeedback) { me.movePointer(x, y); if (me.touchCount) { // Currently there is no support for multi-touch playback, so we'll just move // the most recent indicator around with the mouse pointer. // TODO: handle multi-touch indicator = indicators[indicators.length - 1]; me.moveGestureIndicator(indicator, x, y); } } me.x = x; me.y = y; }, detachPointer: function () { var el = this.pointerElement, parentNode = el && el.parentNode; if (parentNode) { this.pointerElement = parentNode.removeChild(el); } }, movePointer: function(x, y) { var pointerElement = this.pointerElement; if (!pointerElement.parentNode) { document.body.appendChild(pointerElement); } pointerElement.style.display = ''; pointerElement.style.top = y + 'px'; pointerElement.style.left = x + 'px'; }, hidePointer: function() { this.pointerElement.style.display = 'none'; }, showGestureIndicator: function() { var me = this, wrap, inner, indicator; if (me.visualFeedback) { wrap = document.createElement('div'); inner = document.createElement('div'); wrap.appendChild(inner); wrap.className = 'orion-gesture-indicator-wrap'; inner.className = 'orion-gesture-indicator'; wrap.style.top = me.y + 'px'; wrap.style.left = me.x + 'px'; document.body.appendChild(wrap); indicator = { isAnimatingSize: true, wrap: wrap, inner: inner }; // css transitions on newly created elements do not work unless we first trigger // a repaint. inner.offsetWidth; inner.className += ' orion-gesture-indicator-on'; function end() { indicator.isAnimatingSize = false; inner.removeEventListener('transitionend', end); } inner.addEventListener('transitionend', end); me.gestureIndicators.push(indicator); } }, hideGestureIndicator: function(indicator) { var me = this, indicators = me.gestureIndicators, wrap, inner; if (!indicator && indicators.length) { indicator = indicators[0]; } if (indicator) { wrap = indicator.wrap; inner = wrap.firstChild; ST.Array.remove(indicators, indicator); if (indicator.isAnimatingSize) { // If the size animation is still underway, wait until it completes // to perform the fade animation function doneAnimatingSize() { ST.defer(function() { // css transitions do not seem to work properly when run in // immediate succession, hence the need for the slight delay here. me.hideGestureIndicator(indicator); }, 10); inner.removeEventListener('transitionend', doneAnimatingSize); } inner.addEventListener('transitionend', doneAnimatingSize); } else { inner.addEventListener('transitionend', function () { // finished fade-out transition - remove from dom if (wrap.parentNode) { document.body.removeChild(wrap); // ensure pointer is detached me.detachPointer(); } }); inner.className += ' orion-gesture-indicator-off'; } ST.defer(function() { // worst case scenario - transitionend did not fire - cleanup dom if (wrap.parentNode) { document.body.removeChild(wrap); // ensure pointer is detached me.detachPointer(); } }, 900); } }, moveGestureIndicator: function(indicator, x, y) { if (indicator) { var wrap = indicator.wrap; wrap.style.top = y + 'px'; wrap.style.left = x + 'px'; } }, /** * @private * Applies override to gesture publisher if applicable */ setupGesturePublisher: function () { var hasDispatcher = false, isAsync = true, publisher, gestureInstance; if (Ext && Ext.event) { hasDispatcher = !!Ext.event.Dispatcher; gestureInstance = Ext.event.publisher && Ext.event.publisher.Gesture && Ext.event.publisher.Gesture.instance; if (gestureInstance) { // 5.1.0+ have a gesture instance, but in 6.2.0, async is no longer a config and events are synchronous isAsync = gestureInstance && gestureInstance.getAsync ? gestureInstance.getAsync() : false; } // 5.0.0+ if (hasDispatcher) { publisher = Ext.event.Dispatcher.getInstance().getPublisher('gesture'); } // 5.1.0-6.0.x; in 6.2.0, events are synchronous if (gestureInstance && isAsync) { publisher = gestureInstance; } } if (publisher) { Ext.override(Ext.event.publisher.Gesture, { publish: function (eventName, target, e) { var me = this, queue = me.gestureQueue; if (e.event && e.event.eventId && queue) { queue.add(eventName, e.event.eventId); } me.callParent(arguments); } }); // if we have a publisher, set up a gesture queue that we can interrogate later publisher.gestureQueue = ST.gestureQueue = new ST.event.GestureQueue(publisher); } }, /** * Starts the {@link ST.event.Recorder event recorder}. Once this method is called * all further test execution is halted. * * This method is typically injected automatically by Sencha Test Studio when using * its Event Recorder and is therefore rarely called directly. * @method startRecording * @member ST */ startRecording: function () { ST.logger.trace('context.Local.startRecording'); var me = this, recorder = me.recorder || (me.recorder = new ST.event.Recorder()), sendMessage; if (ST.options.driverConfig) { sendMessage = ST.defaultContext.sendMessage; } else { sendMessage = ST.sendMessage; } recorder.throttle = 0; try { recorder.on({ scope: ST, add: function (recorder, events) { sendMessage({ type: 'recordedEvents', events: events }); }, stop: function () { sendMessage({ type: 'recordingStopped' }); }, start: function () { sendMessage({ type: 'recordingStarted' }); } }); recorder.start(); } catch (err) { if (recorder) { console.log(recorder); } console.error(err.message || err); console.error(err.stack || err); } }, toggleInspectEnabled: function (message) { return ST.inspector.Inspect.getInstance().toggleInspectEnabled(message); }, inspectQuery: function (msg) { return ST.inspector.Inspect.getInstance().highlightQuery(msg.locator); }, inspectBatch: function (msg) { return ST.inspector.Inspect.getInstance().batchQuery(msg.locators); }, inspectAllProperties: function (msg) { var cmp = Ext.ComponentQuery.query('[id='+msg.elId+']')[0], // TODO guard against multiples? Should never happen right? Duplicate cmp ids? properties = [], ignoreList = ['plugins', 'frameIdRegex', 'idCleanRegex', 'validIdRe']; for (var key in cmp) { var value = cmp[key]; if (key === 'itemId' && cmp['itemId'] === cmp['id']) { continue; } // skip all tpl and xyzTpl if (/(^tpl)|(.*Tpl)$/.test(key)) { continue; } if (value != null && typeof value != 'undefined' && !Ext.Array.contains(ignoreList, key) && !Ext.isEmpty(value) && !Ext.isObject(value) && !Ext.isArray(value) && !Ext.isFunction(value)) { properties.push({ name: key, value: value.toString() }); } //if (cmp.hasOwnProperty(key)) {} } return properties; }, pollInspector: function () { var me = this, inspector = ST.inspector.Inspect.getInstance(), data = inspector.socketEventQueue.splice(0); // if we are going to poll, maybe we missed something so grab it first while (data && data.data && data.event) { var msg = data.data; msg.type = data.event; ST.sendMessage(msg); data = inspector.socketEvent.Queue.splice(0); } window._inspectCallback = function (data) { var msg = data.data; msg.type = data.event; ST.sendMessage(msg); me.pollInspector(); } }, refreshTrees: function () { var inspector = ST.inspector.Inspect.getInstance(); inspector.componentTree = null; ST.sendMessage({ type: 'inspectEvent', componentTree: JSON.stringify(inspector.getComponentTree()), domTree: JSON.stringify(inspector.getDomTree()), url: location.toString() }); }, /** * Local context we start the inspector and the poller. * * inspectEvent messages are sent from browser -> studio reporter via bi-directional AJAX request/poll * in this case I can just listen for a message from studio highlightQuery and do the work. * * For WebDriver it will be tricker as I can only really do uni-directional communication easily so will * have to "switch" the direction between inspector grid clicking and inspector wizard highlighting. */ startInspector: function () { var me = this, sendMessage, inspector; if (ST.options.driverConfig) { sendMessage = ST.defaultContext.sendMessage; } else { sendMessage = ST.sendMessage; } sendMessage({type: 'recordingStarted'}); inspector = ST.inspector.Inspect.getInstance(); sendMessage({ type: 'inspectEvent', componentTree: JSON.stringify(inspector.getComponentTree()), domTree: JSON.stringify(inspector.getDomTree()), url: location.toString() }); me.pollInspector(); }, execute: function (playable, fn, restParams, resolve, reject) { try { restParams.unshift(playable.future._value()); var result = fn.apply(playable, restParams); resolve(result); } catch (e) { reject(e.message || e); } }, getUrl: function (fn) { fn(location.toString()); }, getTitle: function (fn) { fn(document.title); }, url: function (url, done) { url = new ST.Url(url); done = typeof done === 'function' ? done : ST.emptyFn; if (url.isOnlyHash()) { location.hash = url.getHash(); } else { throw new Error('For in-browser tests, ST.navigate() can only be used for fragment URLs, not for full redirection.'); } done(); }, setViewportSize: function (width, height, done) { ST.system.setViewportSize(width, height, done); }, /** * Takes a snapshot of the viewport and compares it to the associated baseline image. * @param {String} name * @param {Function} done * @method screenshot * @member ST */ _screenshot: function (name, done) { ST.system.screenshot({ name: name }, function (comparison, err) { done(err, comparison); }); }, _checkGlobalLeaks: function (done) { var result = { results: [], addedGlobals: [] }; try { result = ST.checkGlobalLeaks(); } catch (e) { result.results.push({ passed: false, message: 'Global leaks check threw an error ' + (e && e.message) || e }); } done(result); } });