/** * @class ST.context.WebDriver */(function() { var DEBUG_ME = false, WebDriver; ST.context.WebDriver = ST.define({ extend: ST.context.Base, /** * @cfg {Object} driverConfig * Object which should contain a desiredCapabilities property which conforms to the * webdriver desired capabilities {@link https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities} * specification. Such as: * * var ctx = new ST.context.WebDriver({ * driverConfig: { * desiredCapabilities: { * browserName: 'chrome' * } * } * }); */ /** * @cfg {String} subjectUrl * The URL to open when init() is called. */ /** * @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: inherited from Player */ eventTranslation: true, /** * @cfg {Boolean} visualFeedback * `false` to disable visual feedback during event playback (mouse cursor, and "gesture" * indicator) * NOTE: inherited from Player */ visualFeedback: true, _STContent: "", isWebDriverContext: true, hbRE: /.*STHB:/, msgRE: /.*STM:/, /** * Construct a new WebDriver context and pre-fetch the ST context. */ constructor: function (config) { var me = this; ST.context.WebDriver.superclass.constructor.call(me, config); ST.apply(me, config); me.targetMessages = []; me.targetLogs = []; // TODO put these in the magical Studio preferences file for manual tweaking in the field. me.logPollInterval = 500; me.heartbeatInterval = 250; me.heartbeatFailureThreashold = 3000; me._prefetchSTContent(); }, /** * Opens the desired browser, initializes it, loads the subjectUrl in the browser, * and loads Sencha Test framework into target browser so that the context is ready * for use with Futures API methods. */ init: function () { var me = this; return new Promise(function (resolve, reject) { if (DEBUG_ME) debugger me.driver = ST.webdriverio .remote(me.driverConfig) .init() .then(function (ret) { if (ST.LOGME) console.log('webdriver init ret='+JSON.stringify(ret)); if (ret.value && ret.value.browserName && ret.value.browserName === 'chrome') { me.LOGS_SUPPORTED = true; me.pollLogs(); } else { me.LOGS_SUPPORTED = false; } }, function (err) { var message; if (err.seleniumStack) { message = err.seleniumStack; } else { message = err.message || err; } reject(message); }) .timeouts('script', 10 * 1000) .then(function (ret) { me.driver.addCommand('STmoveToObject',me._moveToObject) if (me.subjectUrl) { me.driver.url(me.subjectUrl).then(function () { me .initBrowserInfo() .then(function () { me._loadST().then(resolve, reject); }); }, function (err) { reject('Error loading url: ' + me.subjectUrl + ', err: ' + err); }); } else { if (ST.LOGME) console.log('no subjectUrl in WebDriver constructor config options...'); resolve(); } }) .catch(function (err) { if (ST.LOGME) console.log('webdriver init err=',err); reject(err); }); }); }, initEvent: function (event) { if (!ST.playable.Playable.isRemoteable(event)) { return this.createPlayable(event); } else { // for WebDriver we want the player to keep the "bare" Playable config // since we create the playable in the target only. return event; } }, initBrowserInfo: function () { var me = this, driver = me.driver, getBrowserInfo = me.remoteHelpers.getBrowserInfo; return driver.execute(getBrowserInfo) .then(function (result) { var browserInfo = result.value; ST._initBrowser(browserInfo.navigator.userAgent); ST._initOS({ navigator: browserInfo.navigator, window: browserInfo.window }) }); }, _prefetchSTContent: function () { var fs = require('fs'), path = require('path'), serveDir = ST.serveDir, files = [ 'init.js', 'supports.js', 'base.js', 'Version.js', 'common.js', 'Timer.js', 'setup-ext.js', 'context/Base.js', 'context/Local.js', 'context/LocalWebDriver.js', 'Browser.js', 'OS.js', 'Element.js', 'KeyMap.js', 'Alert.js', 'event/Event.js', 'event/GestureQueue.js', 'event/wgxpath.install.js', 'Locator.js', 'locator/Strategy.js', 'event/Driver.js', 'event/Recorder.js', 'playable/Playable.js', 'playable/State.js', 'future/Element.js', 'future/Component.js', 'tail.js' ], content = {}; for (var i=0; i<files.length; i++) { content[files[i]] = fs.readFileSync(path.join(serveDir, files[i]), {encoding:'utf8'}); } this._STContent = content; }, getLogs: function () { var me = this, driver = me.driver, logItems, logMessage, message; if (!me.logPromise && me.LOGS_SUPPORTED) { me.logPromise = driver.log('browser').then(function (logs) { me.logPromise = null; logItems = logs.value; logItems.forEach(function (log) { logMessage = log.message; if (me.msgRE.test(logMessage)) { /** Log Detection * We have to do some logging-type detection * Chrome logs thusly: * console-api 141:16 "STM:{\"to\":\"runner\",\"type\":\"recordingStarted\",\"seq\":1}" * Embedded logs thusly: * console-api 142:17 STM:{"to":"runner","type":"recordingStarted","seq":1} */ if (logMessage.indexOf('"STM:') !== -1) { // Start with Chrome detection // Found Chrome log type logMessage = JSON.parse(logMessage.substr(logMessage.indexOf('"STM:'))); message = JSON.parse(logMessage.substr(logMessage.indexOf('STM:') + 4)); } else { message = JSON.parse(logMessage.substr(logMessage.indexOf('STM:') + 4)); } if (message.to === 'runner') { ST.sendMessage(message); } else if (message.to !== 'sandbox') { debugger; } else { if (message.type === 'navigate') { if (me.isRecording) { me._loadST().then(function () { me.startRecording(); }, function (err) { // Problem loading ST if (ST.LOGME) console.log('==========> problem after naviagtion'); me.handleClientError(err); }).catch(function (err) { // Problem starting recorder me.handleClientError(err); }); } } } } else if (me.hbRE.test(log)) { if (ST.LOGME) console.log('WebDriver.getLogs: got heartbeat message'); // TODO update lastheartbeattime thingamabob } else { me.targetLogs.push(log); } }); me.logTargetConsole(); // TODO determine failure threshold(s), execute either an ST reload or utter failure back to Studio // Need to use the heartbeating bits to determine connectivity to target browser before full failure }, function (err) { if (ST.LOGME) console.log('WebDriver.getLogs: Log retrieval failed.', err); me.handleClientError(err); }).catch(function (err) { me.handleClientError(err); }); } return me.logPromise; // TODO: error handling of log polling... // error when browser quits in selenium log: // Unable to evaluate script: disconnected: not connected to DevTools // but that error isn't returned in the driver log response... boo }, handleClientError: function (error) { // init handles webdriver launch errors, so we want to know when something happened. // maybe we don't care if the target went away and should just stop whatever Studio was doing. if (error.type === 'NoSessionIdError') { // Target browser went away // TODO send message to Studio? } else if (error.type === 'RuntimeError') { if (error.seleniumStack) { if (error.seleniumStack.status === '6') { // Target browser went away // TODO send message to Studio? } } } else { debugger; } }, logTargetConsole: function () { var me = this, logItems = me.targetLogs; if (!me.logPromise) { me.targetLogs = []; if (logItems.length) { logItems.forEach(function (log) { if (ST.LOGME) { console.log(ST.LOGME+': '+log.message); ST.sendMessage({ type: 'targetLog', message: ST.LOGME+': '+log.message }); } }); } } }, pollLogs: function () { var me = this; if (!me.logTimer && me.LOGS_SUPPORTED) { me.logTimer = setInterval(function () { me.getLogs(); }, me.logPollInterval); } }, startTargetHeartbeat: function () { var me = this, driver = me.driver; driver.execute(function (_heartbeatInterval) { // IN-BROWSER-BEGIN ST.defaultContext.startHeartbeat(_heartbeatInterval); // IN-BROWSER-END }, me.heartbeatInterval).catch(function (err) { // TODO couldn't start the heartbeat, probably should fallback to Studio if (ST.LOGME) console.log('Something happened on the way to heartbeat.', err); me.handleClientError(err); }); }, isRecordingPlayable: function (playable) { return typeof playable.recorderId !== 'undefined'; }, remoteHelpers: { getBrowserInfo: function () { return { navigator: { userAgent: navigator.userAgent, platform: navigator.platform }, window: { deviceType: window.deviceType, location: { search: window.location.search || '' } } } }, getSelectorForTarget: function (target) { var el = typeof target === 'object' ? target : ST.Locator.find(target), id = new Date().getTime(), attr = 'data-senchatestid'; // make sure we found something!!! if (el) { // if we don't already have an attribute, add it with the value if (!el.hasAttribute(attr)) { el.setAttribute(attr, id); } // otherwise, we'll just use the one that was previously assigned else { id = el.getAttribute(attr); } // compose the selector, job done!! return "[" + attr + "='" + id + "']"; } else { // no happy results...return root return '@'; } }, isScrollable: function (x, y, el) { var el = el || document.body, dimEl = el === document.body ? document.documentElement : el, scrollSize = ST.getScrollbarSize(), isYScrollable = el.scrollHeight > dimEl.clientHeight, isXScrollable = el.scrolLWidth > dimEl.clientWidth, isScrollable = isYScrollable || isXScrollable, box = el.getBoundingClientRect(), rightGap = box.right - scrollSize.width, bottomGap = box.bottom - scrollSize.height; return isScrollable && (x >= (rightGap) || (y >= bottomGap)); }, handleWheel: function (_coords, _deltas, _element) { var me = this, el = _element || document.elementFromPoint(_coords.x, _coords.y), oldScroll = {}; // this method inspects the element and determines if the scroll position is such that it can // respond to the passed inputs // we want to be able keep going up the node tree if the target for scrolling is already at its // min/max limits, so that the scroll can continue to "overflow" up the hierarchy, like it does // with an actual browser scroll var canScroll = function (node, x, y) { var canScrollX = false, canScrollY = false, yScroll = node.scrollHeight > node.clientHeight, xScroll = node.scrollWidth > node.clientWidth, atTop = node.scrollTop === 0, atBottom = (node.scrollHeight - node.clientHeight) === node.scrollTop, atLeft = node.scrollLeft === 0, atRight = (node.scrollWidth - node.clientWidth) === node.scrollLeft; if (x > 0) { canScrollX = xScroll && !atRight; } else if (x < 0) { canScrollX = xScroll && !atLeft; } if (y > 0) { canScrollY = yScroll && !atBottom; } else if (y < 0) { canScrollY = yScroll && !atTop; } return canScrollX || canScrollY; }; var isScrollable = function (node) { if (node === null) { return null; } if (node === document.body || canScroll(node, _deltas.x, _deltas.y)) { return node; } else { return isScrollable(node.parentNode); } }; var doScroll = function (node) { var scrollable = isScrollable(node); // the scrollable will either be the passed-in target, or the next scrollable ancestor if (scrollable) { oldScroll.top = scrollable.scrollTop; oldScroll.left = scrollable.scrollLeft; scrollable.scrollTop = scrollable.scrollTop + (_deltas.y); scrollable.scrollLeft = scrollable.scrollLeft + (_deltas.x); // if no scrolling actually occurs, we need to recurse since scrollHeights might be askew for some reason if (scrollable !== document.body && oldScroll.top === scrollable.scrollTop && oldScroll.left === scrollable.scrollLeft) { doScroll(scrollable.parentNode); } } } doScroll(el); }, getScrollPosition: function (_pos, _element) { var el = _element; var isScrollable = function (node) { if (node === null) { return null; } if (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth) { return node; } else { return isScrollable(node.parentNode); } }; var scrollable = isScrollable(el); // the scrollable will either be the passed-in target, or the next scrollable ancestor if (scrollable) { scrollable.scrollTop = _pos[1]; scrollable.scrollLeft = _pos[0]; } }, getElementOffsets: function (x, y, target) { var failOnMultiple = ST.options.failOnMultipleMatches, offsetX = x, offsetY = y, rect, el; // force no failures on multiple matches ST.options.failOnMultipleMatches = false; el = ST.Locator.find(target); // restore settings ST.options.failOnMultipleMatches = failOnMultiple; if (el) { rect = el.getBoundingClientRect(); offsetX = Math.floor(x - rect.left); offsetY = Math.floor(y - rect.top); } return { offsetX: offsetX, offsetY: offsetY }; } }, inject: function (playable, resolve, reject) { var me = this, type = playable.type, isRP = me.isRecordingPlayable(playable), webElement; // for tap/focus/others... target could be me.locator and take care of this? if (playable.webElement || playable.future) { webElement = playable.webElement || playable.future.webElement; // TODO how to resolve this? } else { webElement = {}; } if (playable.args) { playable.x = playable.x || playable.args.x playable.y = playable.y || playable.args.y } if (!playable.args) playable.args = {}; if (DEBUG_ME) debugger; if (!ST.playable.Playable.isRemoteable(playable)) { // if an event is not remoteable and it isn't one of the below then // it is likely a wait(<milliseconds>) call or similar. // debugger } // NOTE: playable.targetEl is a WebJSON Element, so use .ELEMENT to provide the ID to JSONWire protocol // methods below... if (type === 'mousedown') { if (isRP) { me.driver .execute(me.remoteHelpers.getElementOffsets, playable.pageX, playable.pageY, playable.target) .then(function (_offsets) { var offsets = _offsets.value; playable.x = offsets.offsetX; playable.y = offsets.offsetY; playable.xy = [offsets.offsetX, offsets.offsetY]; me.driver .execute(me.remoteHelpers.isScrollable, playable.pageX, playable.pageY, playable.webElement) .then(function (_isScrollbarClick) { playable.isScrollbarClick = _isScrollbarClick.value; // Oh travesty! Oh abomination! But if this is click on a scrollbar, we need to store the element for later // so we can interrogate it for its scroll position. if (playable.isScrollbarClick) { me.scrollClickElement = playable.webElement; } else { // if not a scroll click, for the love of all that is holy, delete it!!! delete me.scrollClickElement; } me.driver.buttonDown().then(resolve,reject); }); }); } else { me.driver .execute(me.remoteHelpers.getSelectorForTarget, playable.webElement) .then(function (_selector) { me.driver // TODO: x/y need to be offsets to the element, currently they are page offsets :( .STmoveToObject(_selector.value, playable.x, playable.y) .buttonDown() .then(resolve, reject); }); } } else if (type === 'mouseup') { if (isRP) { if (me.scrollClickElement) { // if we're in the middle of a yet-to-be-resolved scrollClick, we need to retrieve the scroll position // from the initially clicked element so we can provide those positions when they are needed me.driver.execute(function (_element) { return { x: _element.scrollLeft, y: _element.scrollTop }; }, me.scrollClickElement) .then(function (_scrollPos) { // store the scroll position on the playable so we can retrieve it later when needed playable.finalScrollPosition = _scrollPos.value; me.driver.buttonUp().then(resolve,reject); }); } else { me.driver .execute(me.remoteHelpers.getElementOffsets, playable.pageX, playable.pageY, playable.target) .then(function (_offsets) { var offsets = _offsets.value; playable.x = offsets.offsetX; playable.y = offsets.offsetY; playable.xy = [offsets.offsetX, offsets.offsetY]; me.driver.buttonUp().then(resolve,reject); }); } } else { // no scroll clicking in progress, just run the buttonUp me.driver .execute(me.remoteHelpers.getSelectorForTarget, playable.webElement) .then(function (_selector) { me.driver // TODO: x/y need to be offsets to the element, currently they are page offsets :( .STmoveToObject(_selector.value, playable.x, playable.y) .buttonUp() .then(resolve, reject); }); } } else if (type === 'scroll' ) { me.driver.execute(me.remoteHelpers.getScrollPosition, playable.pos, webElement) .then(resolve,reject); } else if (type === 'wheel' ) { // if we have a web element, we'll want to use it; otherwise, let's null it out so it doesn't cause problems if (typeof webElement !== 'object' || !webElement.ELEMENT) { webElement = null; } if (playable.deltaX !== 0 || playable.deltaY !== 0) { var coords = {x: playable.pageX, y:playable.pageY}, deltas = {x: playable.deltaX, y: playable.deltaY}; me.driver.execute(me.remoteHelpers.handleWheel, coords, deltas, webElement).then(resolve,reject); } } else if (type === 'tap' || type === 'click') { if (isRP) { if (!me.scrollClickElement) { resolve(); } } else { me.driver .execute(me.remoteHelpers.getSelectorForTarget, playable.webElement) .then(function (_selector) { me.driver .STmoveToObject(_selector.value, playable.x, playable.y) .buttonDown() .buttonUp() .then(resolve, reject); }); } } else if (type === 'dblclick') { me.driver .execute(me.remoteHelpers.getSelectorForTarget, playable.webElement) .then(function (_selector) { me.driver .doubleClick(_selector.value) .then(resolve, reject); }); } else if (type === 'rightclick') { // right click is a gesture, so will only ever playback... me.driver .execute(me.remoteHelpers.getSelectorForTarget, playable.webElement) .then(function (_selector) { me.driver // TODO: x/y need to be offsets to the element, currently they are page offsets :( .rightClick(_selector.value, playable.x || 0, playable.y || 0) .then(resolve, reject); }); } else if (type === 'type') { // TODO support args.caret var text = playable.args.text || playable.text, key = playable.args.key || playable.key, caret = playable.args.caret || playable.caret; text = text || key; if (key === 'Backspace') { text = String.fromCharCode(8); } else if (key === 'ArrowLeft') { text = 'Left arrow'; } else if (key === 'ArrowRight') { text = 'Right arrow'; } else if (key === 'ArrowUp') { text = 'Up arrow'; } else if (key === 'ArrowDown') { text = 'Down arrow'; } else if (key === 'CapsLock') { resolve(); return; } if (webElement) { me.driver.elementIdValue(webElement.ELEMENT, text).then(resolve, reject); } else { me.driver.keys(text).then(resolve, reject); } } else if (playable.type === 'keydown') { var key = playable.args.key || playable.key; if (key === 'Backspace') { // TODO FnKeys me.driver.keys(String.fromCharCode(8)).then(resolve, reject); } else if (key === 'ArrowLeft') { me.driver.keys('Left arrow').then(resolve, reject); } else if (key === 'ArrowRight') { me.driver.keys('Right arrow').then(resolve, reject); } else if (key === 'ArrowUp') { me.driver.keys('Up arrow').then(resolve, reject); } else if (key === 'ArrowDown') { me.driver.keys('Down arrow').then(resolve, reject); } else if (key === 'CapsLock') { resolve(); } else { me.driver.keys(key).then(resolve, reject); } } else if (playable.type === 'keyup') { // webdriverio leaves modifier keys 'pressed' until pressed again // http://webdriver.io/api/protocol/keys.html if (playable.key === 'Shift' || playable.key === 'Control' || playable.key === 'Alt' || playable.key === 'Meta') { // TODO add other modifier keys (Fn, PgUp/Dn, etc.)? // release all the keys me.driver.keys('NULL').then(resolve, reject); } else { resolve(); } } else if (playable.type && playable.type !== 'wait') { resolve(); } else { resolve(); } }, _remoteCallFn: function (playable, serialized, retry) { if (ST.LOGME) console.log('_remoteCallFn() serialized=',serialized); var me = this, driver = me.driver, ser = serialized; return new Promise(function (resolve, reject) { var start = new Date().getTime(); if (DEBUG_ME) { driver.timeouts('script', 30 * 60 * 1000); } driver.executeAsync(function (event, debug, remoteDone) { // IN-BROWSER-BEGIN if (typeof ST === 'undefined') { remoteDone({ loadST: true }); throw('ST_NEED_ST_EXCEPTION'); } if (ST.LOGME) console.log('call, event=',event); ST.DEBUG = debug; window.$ST = window.$ST || {}; window.$ST.call = function () { if (ST.DEBUG) debugger var playable = ST.defaultContext.createPlayable(event), future = playable.future, promise, start; start = new Date().getTime(); if (typeof playable.fn !== 'function') { remoteDone({ error: 'playable.fn is not a function' }); return; } var localDone = function (result) { var retValue = { playable: { webElement: playable.targetEl && playable.targetEl.dom }, future: { webElement: future.el && future.el.dom }, data: playable.future.data, locatorWebElement: future.locator && future.locator.targetEl && future.locator.targetEl.dom, duration: new Date().getTime() - start }; retValue.error = result && result.error; remoteDone(retValue); }; localDone.fail = function (err) { localDone({ error: err.toString() }); }; promise = playable.fn.call(playable, localDone); if (promise && typeof promise.then === 'function') { promise.then(function () { localDone(); }, function (err) { localDone({ error: err.toString() }); }); } else if (!playable.fn.length) { localDone(); } }; window.$ST.call(); // IN-BROWSER-END }, ser, DEBUG_ME).then(function (ret) { if (ST.LOGME) console.log('WebDriver._remoteCallFn, executeAsync resolved with '+JSON.stringify(ret)); if (DEBUG_ME) debugger if (ST.options.webdriverPerf) { ST.status.addResult({ passed: true, message: 'WebDriver._remoteCallFn.browser.duration.'+playable.type+'='+ret.value.duration }); ST.status.addResult({ passed: true, message: 'WebDriver._remoteCallFn.sandbox.duration.'+playable.type+'='+(new Date().getTime()-start) }); } var v = ret.value, loadST = v.loadST, error = v.error; if (error) { reject(error); return; } if (loadST && retry) { me._loadST().then(function () { me._remoteCallFn(playable, ser, false).then(resolve, reject); }, reject); } else { { ST.apply(playable, v.playable); // apply targetDom because it might have been reconstructed ST.apply(playable.future, v.future); if (v.data && playable.future) { playable.future.setData(v.data); } if (v.locatorWebElement) { playable.future.locator.webElement = v.locatorWebElement; } resolve(ret.value); } } }, function (err) { if (ST.LOGME) console.log('callFn remote, err=',err); if (DEBUG_ME) debugger if (err.seleniumStack && err.seleniumStack.type === 'StaleElementReference') { playable.webElement = undefined; playable.future.webElement = undefined; reject(); return; } reject(err); }).catch(function (err) { if (ST.LOGME) console.log('callFn remote catch err=', err); }); }); }, callFn: function (playable, done) { var me = this, serialized = me._serialize(playable); if (DEBUG_ME) debugger if (ST.playable.Playable.isRemoteable(playable)) { return me._remoteCallFn(playable, serialized, true); // one retry } else { return playable.fn.call(playable, done); } }, _serialize: function (playable) { var ret = {}; for (var i in playable) { var t = typeof playable[i]; var v = playable[i]; if (t !== 'object' && t !== 'function') { ret[i] = v; } if (t === 'object' && v === null) { ret[i] = v; } } ret.webElement = playable.webElement; if (playable.target) { if (typeof playable.target === 'string') { ret.target = playable.target; } else { ret.target = {}; ret.target.webElement = playable.target.webElement; ret.target.locatorChain = playable.target.locatorChain; } } // take relatedCmps, strip out their webElement and send that instead... ret.future = ret.future || {}; ret.futureData = ST.apply({}, playable.instanceData); if (playable.future) { var retf = ret.future, future = playable.future, related = future.related; retf.related = {}; for(var name in related) { var relatedFuture = related[name]; retf.related[name] = { webElement: relatedFuture.webElement, futureClsName: relatedFuture.$className, data: relatedFuture.data }; } if (future.locator) { retf.locator = { webElement: future.locator.webElement, locatorChain: future.locator.locatorChain }; } retf.locatorChain = ret.root = future.locatorChain; retf.webElement = future.webElement; ret.futureClsName = future.$className; retf.data = future.data; retf.futureData = future.futureData; } ret.args = playable.args; ret.instanceData = playable.instanceData; return ret; }, _remoteReadyFn: function (playable, serialized, retry) { if (ST.LOGME) console.log('_remoteReadyFn() serialized=',serialized); var me = this, driver = me.driver, ser = serialized; return new Promise(function (resolve,reject) { var start = new Date().getTime(); if (DEBUG_ME) { driver.timeouts('script', 30 * 60 * 1000); } driver.executeAsync(function (event, debug, done) { var start = new Date().getTime(); // IN-BROWSER-BEGIN if (typeof ST === 'undefined') { done({ loadST: true }); } ST.DEBUG = debug; window.$ST = window.$ST || {}; window.$ST.ready = function () { if (ST.DEBUG) debugger var playable = ST.defaultContext.createPlayable(event), future = playable.future, ready = false; try { ready = playable.ready(); } catch (e) { if (ST.DEBUG) debugger done({ error: e.message, duration: new Date().getTime() - start }); return; } done({ playable: { webElement: playable.targetEl && playable.targetEl.dom, waitingFor: playable.waitingFor, waitingState: playable.waitingState }, future: { webElement: future.el && future.el.dom }, data: future.data, ready: ready, duration: new Date().getTime() - start }); }; window.$ST.ready(); // IN-BROWSER-END }, ser, DEBUG_ME).then(function (ret) { if (ST.LOGME) console.log('ready remote call playable.type='+playable.type+', ret=',ret); if (ST.options.webdriverPerf) { ST.status.addResult({ passed: true, message: 'WebDriver._remoteReadyFn.browser.duration.'+playable.type+'='+ret.value.duration }); ST.status.addResult({ passed: true, message: 'WebDriver._remoteReadyFn.sandbox.duration.'+playable.type+'='+(new Date().getTime()-start) }); } if (DEBUG_ME) debugger var v = ret.value, ready = v.ready, error = v.error, loadST = v.loadST; if (loadST && retry) { me._loadST().then(function () { me._remoteReadyFn(playable, ser, false).then(resolve, reject); }, reject); } else if (error) { reject(error); } else { ST.apply(playable, v.playable); ST.apply(playable.future, v.future); if (v.data && playable.future) { playable.future.setData(v.data); } ready ? resolve(ret) : reject(); } }, function (err) { if (ST.LOGME) console.log('ready remote call, playable.type='+playable.type+', err=',err); if (DEBUG_ME) debugger if (err.seleniumStack && err.seleniumStack.type === 'StaleElementReference') { playable.webElement = undefined; playable.future.webElement = undefined; reject(); return; } if (retry) { me._loadST().then(function () { me._remoteReadyFn(playable,ser,false).then(resolve,reject); }, reject); } else { reject(err); } }).catch(function (err) { if (ST.LOGME) console.log('ready remote executeAsync catch, playable.type='+playable.type+', err='+err); }); }); }, ready: function (playable, resolve, reject) { var me = this, serialized = me._serialize(playable); if (ST.playable.Playable.isRemoteable(playable)) { me._remoteReadyFn(playable, serialized, true /* one retry */).then(resolve, reject); } else { resolve(); } }, _checkGlobalLeaks: function (done, contextGlobals, retry) { if (ST.LOGME) console.log('WebDriver._checkGlobalLeaks()'); var me = this, driver = me.driver, contextGlobals = contextGlobals || ST.options.contextGlobals; if (typeof retry === 'undefined') { retry = true; } driver.executeAsync(function (debug, contextGlobals, done) { if (typeof ST === 'undefined') { done({ loadST: true }); return; } ST.DEBUG = debug; window.$ST = window.$ST || {}; window.$ST.check = function () { try { var result; if (ST.DEBUG) debugger ST.addGlobals.apply(ST,contextGlobals); result = ST.checkGlobalLeaks(); done({ result: result }); } catch (e) { done({ error: e.message || e }); } }; window.$ST.check(); }, DEBUG_ME, contextGlobals).then(function (ret) { var value = ret.value, loadST = value.loadST, result = value.result, error = value.error; if (DEBUG_ME) debugger if (loadST && retry) { me._loadST().then(function () { me._checkGlobalLeaks(done, contextGlobals, false); }, function (err) { done({ results: [{ passed: false, message: 'Global leaks check threw an error: ' + (err || err.message) }] }); }); return; } // NOTE: done must return what Base.checkGlobalLeaks expects, currently: // { results: [<expectations results>], addedGlobals: [<string property names>] } if (result) { done(result); } else if (error) { done({ results: [{ passed: false, message: 'Global leaks check threw an error: ' + error }] }); } else { done({ results: [{ passed: false, message: 'Global leaks check returned no results or error.' }] }); } },function (err) { if (DEBUG_ME) debugger done({ results: [{ passed: false, message: 'Global leaks check threw an error: ' + err.message || err }] }); }); }, _loadST: function () { if (ST.LOGME) console.log('WebDriver._loadST()'); var me = this, driver = me.driver, promises = [], files = [], errors = []; if (me.loadingST) { return me.loadingST; } me.loadingST = new Promise(function (resolve, reject) { me.initRecorderModal().then(function (ret) { for (var file in me._STContent) { files.push(file); } var _load = function (i) { // exit recursion if (i > files.length - 1) { if (errors.length > 0) { me.loadingST = null; reject(errors); } else { var tmpOptions = {}; ST.apply(tmpOptions, ST.options); // clear out globals and globalPatterns since we let the target manage it's own. tmpOptions.globalPatterns = {}; tmpOptions.globals = {}; me.driver.executeAsync(function (testOptions, done) { ST.setupOptions(testOptions); // results in ST.initGlobals() being called. if (Ext && Ext.onReady) { // this is an ext page if (ST.LOGME) console.log('setup Ext.onReady callback'); Ext.onReady(function () { if (ST.LOGME) console.log('Ext.onReady callback is called'); ST.initExtJS(); // in case the _beforereadyhandler didn't fire // and the delayed call didn't fire at the right time either. if (ST.LOGME) console.log('after initExtJs called, ST.ready.blocks=' + ST.ready.blocks); done(); }); } else { if (ST.LOGME) console.log('no Ext and Ext.onReady so assume NOT an ext page and return'); done(); // this is a non ext page } }, tmpOptions).then(function () { me.loadingST = null; resolve(); }, function () { me.loadingST = null; reject(); }); } return; } var file = files[i], content = me._STContent[file]; driver.executeAsync(function (file, content, done) { // IN-BROWSER-BEGIN var insert = function () { try { var script = document.createElement('script'); script.innerHTML = content; script.setAttribute('file', file); document.body.appendChild(script); done({ file: file, loaded: true }); } catch (e) { done({ file: file, error: e }); } }; if (document.readyState === 'complete') { insert(); } else { document.addEventListener('readystatechange', function () { if (document.readyState === 'complete') { insert(); } }); } // IN-BROWSER-END }, file, content) .then(function (ret) { if (ret.value && ret.value.loaded) { if (ST.LOGME) console.log('LOADED script:' + ret.value.file); _load(i + 1); } else { if (ST.LOGME) console.log('script:' + ret.value.file + ', appendChild error=', error); errors.push(ret.value); me.loadingST = null; reject(errors); } }, function (err) { if (ST.LOGME) console.log('script:' + file + ', execution err='+JSON.stringify(err)); me.loadingST = null; reject(err); }); }; _load(0); // start the recursion }).then(function () { me.dismissRecorderModal(); }); }); return me.loadingST; }, initRecorderModal: function () { var me = this; return me.driver.execute(function () { // IN-BROWSER BEGIN var modal = document.getElementById('StudioModal'), modalDialog, modalText, _text = 'Sencha Test Event Recorder is loading, this message will be dismissed when ready to record.'; if (!modal) { modal = document.createElement('div'); modalDialog = document.createElement('div'); modalText = document.createElement('span'); modal.id = 'StudioModal'; modal.style.display = 'block'; modal.style.position = 'fixed'; modal.style.zIndex = '1'; modal.style.top = '0'; modal.style.left = '0'; modal.style.width = '100%'; modal.style.height = '100%'; modal.style.paddingTop = '100px'; modal.style.backgroundColor = 'rgb(0,0,0)'; modal.style.backgroundColor = 'rgba(0,0,0,0.4'; modalDialog.style.margin = 'auto'; modalDialog.style.position = 'absolute'; modalDialog.style.top = '10%'; modalDialog.style.left = '10%'; modalDialog.style.width = '80%'; modalDialog.style.height = '100px'; modalDialog.style.padding = '20px'; modalDialog.style.borderWidth = '2px'; modalDialog.style.borderStyle = 'solid'; modalDialog.style.borderColor = '#025B80'; modalDialog.style.backgroundColor = '#FFFFFF'; modalDialog.style.overflow = 'auto'; modalText.innerHTML = '<p>' + _text + '</p>'; modalText.style.backgroundColor = '#FFFFFF'; modalText.style.overflow = 'auto'; modalText.style.wordWrap = 'normal'; modalText.style.textAlign = 'center'; modalDialog.appendChild(modalText); modal.appendChild(modalDialog); document.body.appendChild(modal); } // IN-BROWSER END }); }, dismissRecorderModal: function () { var me = this; return me.driver.execute(function () { // IN-BROWSER BEGIN var modal = document.getElementById('StudioModal'); if (modal) { modal.style.display = 'none'; while (modal.firstChild) { modal.removeChild(modal.firstChild); } document.body.removeChild(modal); } // IN-BROWSER END }); }, cleanup: function () { if (ST.LOGME) console.log('WebDriver cleanup()'); }, /** * Close the browser and shutdown the driver. * * When creating a WebDriver context manually in a test the stop method should be called in an * afterEach or afterAll method. */ stop: function (resolve, reject) { var me = this, logPromise; if (ST.LOGME) console.log('WebDriver stop()'); if (ST.LOGME) console.trace(); if (!me.isRecording) { if (me.driver && me.LOGS_SUPPORTED) { me.logTimer && clearInterval(me.logTimer); logPromise = me.getLogs(); logPromise.then(function (ret) { if (ST.LOGME) console.log('WebDriver.stop, logPromise resolve, call driver.end(), ret=',ret); me.driver.end().then(resolve, reject); }, function (err) { if (ST.LOGME) console.log('WebDriver.stop, logPromise reject, call driver.end(), err=',err); me.driver.end().then(resolve, reject); }); } else { if (ST.LOGME) console.log('WebDriver.stop, no logPromise, call driver.end()'); me.driver.end().then(resolve, reject); } } }, onEnd: function (resolve) { if (ST.LOGME) console.log('WebDriver onEnd()'); if (ST.LOGME) console.trace(); resolve(); }, startRecording: function () { var me = this, driver = me.driver; me.isRecording = true; driver.execute(function () { // IN-BROWSER-BEGIN ST.defaultContext.startRecording(); // IN-BROWSER-END }).then(function (ret) { if (ST.LOGME) console.log('WebDriver.startRecording: recorder started in target browser.'); }, function (err) { // TODO handle recorder errors if (ST.LOGME) console.log('WebDriver.startRecording: Problem loading ST.', err); }); }, stopRecording: function () { var me = this, driver = me.driver; if (driver) { if (me.isRecording) { me.isRecording = false; ST.sendMessage('recordingStopped'); } driver.end(); } }, execute: function (playable, fn, resolve, reject, retry) { var me = this, driver = me.driver, ser = me._serialize(playable); if (typeof retry === 'undefined') { retry = true; } driver.executeAsync(function (event, fnstring, debug, done) { // IN-BROWSER-BEGIN if (typeof ST === 'undefined') { done({ loadST: true }); return; } ST.DEBUG = debug; window.$ST = window.$ST || {}; window.$ST.exec = function () { if (ST.DEBUG) debugger eval('var fn = '+fnstring); var playable = ST.defaultContext.createPlayable(event), future = playable.future, value = future._value(), result; try { result = fn.call(playable, value); done({ result: result }); } catch (e) { done({ error: e.message || e }); } }; window.$ST.exec(); // IN-BROWSER-END }, ser, fn.toString(), DEBUG_ME).then(function (ret) { if (DEBUG_ME) debugger if (ret.value && ret.value.error) { reject(ret.value.error); } else if (ret.value.loadST) { me._loadST().then(function () { me.execute(playable, fn, resolve, reject, false); }, reject); } else { resolve(ret.value.result); } }, function (err) { if (DEBUG_ME) debugger reject(err); }); }, getUrl: function (fn) { this.driver.getUrl().then(fn); }, getTitle: function (fn) { this.driver.getTitle().then(fn); }, url: function (url, done) { var isFragment; url = new ST.Url(url); done = typeof done === 'function' ? done : ST.emptyFn; isFragment = url.isOnlyHash(); url = isFragment ? url.getHash() : url.get(); this.driver.execute(function (url, isFragment) { if (isFragment) { window.location.hash = url; } else { window.location = url; } }, url, isFragment).then(done); }, setViewportSize: function (width, height, done) { this.driver.setViewportSize({ width: width, height: height }).then(done); }, _screenshot: function (name, done) { var me = this; return me.driver.saveScreenshot().then(function (screenshot) { return me._compareScreenshot(name, me._arrayBufferToBase64(screenshot)); }).then(function (comparison) { done(null, comparison); }, function (err) { done(err, null); }); }, _arrayBufferToBase64: function (buffer) { var binary = '', bytes = new Uint8Array(buffer), len = bytes.byteLength, i; for (i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return window.btoa(binary); }, _compareScreenshot: function (name, base64Png) { var me = this; return new Promise(function (resolve, reject) { ST.system.screenshot({ data: base64Png, name: name }, function (comparison, err) { if (err) { reject(err); } else { resolve(comparison); } }); }) }, // in init() we sort of monkey-patch our impl over webdrivers... to fixed // non-mobile center move when offsets are not provided // this code based on webdriverio 3.4.0 lib/commands/moveToObject.js _moveToObject: function (selector, xoffset, yoffset) { /** * check for offset params */ var hasOffsetParams = true; if (typeof xoffset !== 'number' && typeof yoffset !== 'number') { hasOffsetParams = false; } if (this.isMobile || hasOffsetParams) { return this.moveToObject(selector, xoffset, yoffset) } return this.element(selector).then(function (element) { return this.elementIdSize(element.value.ELEMENT).then(function (size) { return this.elementIdLocation(element.value.ELEMENT).then(function (location) { return { element: element, size: size, location: location }; }); }); }).then(function (res) { var _xoffset = (res.size.value.width / 2); var _yoffset = (res.size.value.height / 2); var x = res.location.value.x; var y = res.location.value.y; if (hasOffsetParams) { _xoffset = xoffset _yoffset = yoffset } return this.moveToObject(selector, _xoffset, _yoffset) }); } }); }()) // scope for DEBUG_ME - file scope