(function () {var logger = ST.logger.forClass('event/Player'); ST.event = ST.event || {}; // setup ST.event namespace. /** * @class ST.event.Player * @extend ST.event.Driver * * This class is rarely used directly, but is the underlying mechanism used the inject * events using the {@link ST#play} and {@link ST.future.Element futures} API's. */ST.event.Player = ST.define({ extend: ST.event.Driver, /** * @cfg {Array} events The event queue to playback. This must be provided before * the {@link #method-start} method is called. */ /** * @cfg {Boolean} pauseForAnimations True to pause event playback during animations, false * to ignore animations. Default is true. */ pauseForAnimations: true, /** * @cfg {Boolean} eventTranslation * `false` to disable event translation. If `false` events that are not supported by * the browser's event APIs will simply be skipped. * NOTE: this is passed down to the browser context object. */ eventTranslation: true, /** * @cfg {Boolean} visualFeedback * `false` to disable visual feedback during event playback (mouse cursor, and "gesture" * indicator) * NOTE: this is passed down to the browser context object. */ visualFeedback: true, /** * @cfg {Number} timeout * The event player will wait for certain conditions to be met before playing each * event in the queue. These conditions include: * * - target element present and visible in DOM * - Ext JS framework animations complete * * If the time waited for these conditions to be met exceeds this timeout value, * playback will be aborted. * * If specified, this will be the default {@link ST.playable.Playable#timeout timeout} * for all events. Otherwise, the {@link ST.options#timeout timeout} will be * used. */ /** * @cfg {Number} waitInterval * Amount of time (in milliseconds) to wait before trying again if event cannot be * played due to target element not being available, animations in progress, etc. */ waitInterval: ST.isIE9m ? 50 : 10, paused: 0, /** * This property holds the event last pulled from the queue but yet to be played. */ pendingEvent: null, pendingFn: false, /** * @event timeout * Fires when the player times out while waiting for conditions to be met in order * for playback to proceed, for example, target element unavailable or animations * never finished. * @param {ST.event.Player} this * @param {String} message A message containing the reason for timeout */ constructor: function (config) { var me = this; me.events = []; ST.apply(me, config); if (!me.context) { me.context = ST.defaultContext; } me.context.player = me; me._nextId = 1; }, /** * Adds new {@link ST.playable.Playable events} to the play queue. * * This method is not normally called by user code. Use {@link ST#play} insteaad. * @param [index] * @param {ST.playable.Playable[]} events The events to add or config objects for * them. * @return {ST.playable.Playable[]} The `events` array with each element now promoted * from config object to `ST.playable.Playable` instance. * @private */ add: function (index, events) { var me = this, _events = me.events, length = _events.length, timeout = ST.options.timeout, count, inserting, e, i, queue, t; // ST.logger.debug('Player.add(index=',index,', events=',events,', me.events=',me.events); if (typeof index !== 'number') { events = index; index = null; } count = events.length; // The default behavior is to insert at the beginning of the queue if there is // a "playable" fn further up the call stack. This is primarily to accomodate // nested future calls, for example: // // ST.element('foo').and(function() { // ST.element('bar').and(function() { // foo.innerHTML = 'bar is available'; // }); // }).click(); // // In the above example the events should be played in the order they appear visually. // The "click" event should happen after foo's innerHTML is changed. if (index == null) { // This value is set to 0 by and() before it calls the user's fn. index = me.pendingFn; // And to "false" when we are not inside an and() callback. if (index === false) { index = length; } else { // Each call to a futures method enqueues events here. If these are // made sequentially in an and() callback, we need to preserve their // order by tracking where we left off from the last call. me.pendingFn += count; } } inserting = index < length; // splice() is bugged on IE8 so we avoid it and just slice the events prior to // "index" into a new array. queue = inserting ? (index ? _events.slice(0, index) : []) : _events; for (i = 0; i < count; ++i) { e = events[i]; if (e || e === 0) { // if (not truthy or 0 skip it) var context = e.context || me.context; events[i] = e = context.initEvent(e); if (e.typingDelay === undefined) { e.typingDelay = me.typingDelay; } if (e.visualFeedback === undefined) { e.visualFeedback = me.visualFeedback; } if (e.eventTranslation === undefined) { e.eventTranslation = me.eventTranslation; } // Check for { target: -1, ... } and promote it to use the playable // backwards that number from this entry. // if (typeof(t = e.target) === 'number') { e.target = queue[queue.length + t]; } else if (t && t.dom) { e.targetEl = t.$ST ? t : new ST.Element(t.dom); } if (typeof(t = e.relatedTarget) === 'number') { e.relatedTarget = queue[queue.length + t]; } else if (t && t.dom) { e.relatedTargetEl = t.$ST ? t : new ST.Element(t.dom); } // Only use the default eventDelay for actual "events". Other things // in the queue (like functions added by futures) should run as soon // as possible if (e.delay === undefined && (!e.future) && e.type !== 'wait') { e.delay = me.eventDelay; } if (e.animation === undefined) { e.animation = me.pauseForAnimations; } if (!timeout) { // If ST.options.timeout is 0, ignore all timeouts e.timeout = 0; } else if (e.timeout === undefined) { if ((e.timeout = me.timeout) === undefined) { e.timeout = timeout; } } e._player = me; e.id = me._nextId++; e.state = 'queued'; queue.push(e); } } if (inserting) { // finally we push the events at and beyond "index" onto the new array and // update me.events. for (; index < length; ++index) { queue.push(_events[index]); } me.events = queue; } if (me.active && !me.pendingEvent) { // If there is a pendingEvent then playNext will eventually be called. We // need to call it otherwise, but we must not call it if there is an event // pending as events could be played out of order (each trying to play at // the same time). me.playNext(); } }, cleanup: function () { var me = this, eventTimer = me.eventTimer, context = me.context; me.pendingEvent = null; me.pendingFn = false; if (eventTimer) { me.eventTimer = 0; ST.deferCancel(eventTimer); } me.events.length = 0; if (context) { context.cleanup(); } }, skip: function () { var me = this; me.pendingEvent = null; me.pendingFn = false; me.playNext(); }, onStart: function () { this.playNext(); }, onStop: function () { var me = this, context = me.context; me.cleanup(); if (context) { context.stop(); } }, pause: function () { var me = this, eventTimer = me.eventTimer, event = me.pendingEvent; ++me.paused; if (eventTimer) { me.eventTimer = 0; ST.deferCancel(eventTimer); } if (event && !me.pendingFn) { // Put back the pending event unless we are called from its "fn". me.events.unshift(event); event.state = 'queued'; } }, resume: function () { var me = this; if (me.paused) { if (! --me.paused) { me.playNext(); } } }, isPlaying: function () { return (this.events.length > 0 || this.pendingEvent); }, playNext: function () { var me = this, events = me.events, event; if (!me.pendingEvent && !me.paused) { event = events.shift(); if (!event) { me.onEnd(); } else { if (me.pendingFn) { me.pendingFn--; } me.pendingEvent = event; event.state = 'pending'; me.playEventSoon(event, event.delay || 0); } } }, onEnd: function () { var me = this, context = me.context; context.onEnd(function () { me.fireEvent('end', me); }, function (err) { console.log(err); // eslint-disable-line }); }, playFn: function (event) { logger.debug('Player.playFn, event.type='+event.type+', event.timeout='+event.timeout); var me = this, context = event.context, // user function can either invoke our "done" function when complete // or return a promise. We will not continue to play events until // either the done function is invoked or the promise is resolved. watchDog = new ST.WatchDog(function(err) { logger.debug('watchdog done called, event.type='+event.type); logger.debug('watchdog done called, failed='+failed+', err='+err); if (err) { // if there is waitingFor/waitingState information then timeout so // we present that information instead of the more generic message if (event.waitingFor && event.waitingState) { me.doTimeout(event); } else { me.doError(err); } } else { me.pendingEvent = null; event.state = 'done'; me.playNext(); } }, event.timeout), done = watchDog.done, failed, promise; event.watchdogStarted = new Date().getTime(); // for use in State timer me.pendingFn = 0; if (ST.options.handleExceptions) { try { promise = context.callFn(event, done); } catch (e) { logger.debug('error with context.callFn, e='+e); done.fail(e); // e is sent to watchDog(callback) function above return; } } else { promise = context.callFn(event, done); } me.pendingFn = false; if (promise && typeof promise.then === 'function') { // Return value is "then-able" so it qualifies as a Promise. promise.then(function () { done(); }, function (err) { done.fail(err); // err is sent to watchDog(callback) function above }); } else if (!event.fn.length) { // Check the arity (length) of the Function. If it is 0, the function // does not declare any arguments so assume it has completed. done(); } }, playEvent: function (event) { logger.debug('ST.play(event), event.type='+event.type + ', event.target='+event.target); var me = this, now = +new Date(), waitStartTime = event.waitStartTime, timeout = event.timeout, context = event.context; var notReadyFn = function (err) { logger.debug('playEvent, notReadyFn called, event.type='+event.type + ', err=',err); if (err) { me.doError(err); return; // don't play the event again, we're done. } if (waitStartTime && timeout && ((now - waitStartTime) >= timeout)) { me.doTimeout(event); } else { // Waiting for some condition to be met before we can proceed // such as target element to become available or animations to complete // Try again after a brief delay. if (!waitStartTime) { // a timestamp as of the time we began waiting (for timeout purposes) event.waitStartTime = +new Date(); } me.playEventSoon(event); } }; context.ready(event, function (ret) { logger.debug('Player ready, resolve, ret='+ret); event.state = 'playing'; if (ST.playable.Playable.hasFn(event) || (event.fn && typeof event.fn === 'function')) { me.playFn(event); } else { me.pendingEvent = null; try { context.inject(event, function () { event.state = 'done'; me.playNext(); }, function (err) { notReadyFn(err); }); } catch (e) { notReadyFn(e); } } }, notReadyFn); }, playEventSoon: function (playable, delay) { var me = this; me.eventTimer = ST.defer(function () { me.eventTimer = 0; me.playEvent(playable); }, (delay == null) ? me.waitInterval : delay); }, fail: function(message) { var me = this; // When an error occurs we empty all remaining events from the queue. This // does not "stop" the player - if additional events are added they will be // automatically played. me.cleanup(); if (me.context.isRecording) { ST.sendMessage({ type: 'systemError', message: message + '. Fix the error and try recording again.' }); } me.fireEvent('error', { player: me, message: message }); me.onEnd(); }, doError: function (err) { var message = (err && err.message) || err; this.fail('Failed with error "' + message + '"'); }, doTimeout: function (event) { if (event.state !== 'pending' && event.state !== 'playing') { return; } event.state = 'done'; var s = event.waitingFor || 'event', locator = event[s], toBe = event.waitingState || 'ready', futureClsName = event.futureClsName, src = event, timedout = true, dom, sel, futureTarget; if (s === 'target' || s === 'relatedTarget') { while (src) { if (!(locator = src[s])) { // first check "target" // we might have a targetEl, so use its id or tagName locator = src[s + 'El']; dom = locator && locator.dom; break; } if (locator.isPlayable) { src = locator; // traverse to the target playable for location } else { if (locator.dom) { dom = locator.dom; } else if (typeof locator.nodeType === 'number') { dom = locator; } else { sel = locator.toString(); // handle strings & functions } break; } } if (dom) { // we have a targetEl, so use its id or tagName sel = dom.id; sel = sel ? '#' + sel : dom.tagName; } if (sel) { s += ' (' + sel + ')'; } } else if (src.future.locator.target) { sel = src.future.locator.target; futureTarget = true; } if (futureClsName) { toBe += ' for ' + event.futureClsName; } if (futureTarget) { toBe += ' with target ' + sel; } if (event.expectTimeout) { ST.status.addResult({ passed: true, message: 'expected timeout waiting for ' + s + ' to be ' + toBe }); var me = this; if (!me.events.length) { me.cleanup(); me.onEnd(); return; } else { timedout = false; } } if (timedout) { this.fail('Timeout waiting for ' + s + ' to be ' + toBe); } else { me.skip(); } }});}());