UNPKG

jwebdriver

Version:

A webdriver client for node.js

1,727 lines (1,644 loc) 57.6 kB
'use strict'; /** * This class is used for control browser. * @class Browser */ const fs = require('fs'); const os = require('os'); const path = require('path'); const PromiseClass = require('promiseclass'); const extend = require('xtend'); const Zip = require('node-zip'); const gm = require('gm'); const HostsProxy = require('./hostsproxy'); const Elements = require('./elements'); // default options for browser const defaultBrowserOptions = { 'browserName': 'chrome', 'version': 'ANY', 'platform': 'ANY' }; // nick name for browser const mapBrowserNickName = { 'ie': 'internet explorer', 'ff': 'firefox', 'edge': 'MicrosoftEdge' }; // https://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/click const MouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 2 }; // https://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/element/:id/value const Keys = { NULL: '\uE000', CANCEL: '\uE001', HELP: '\uE002', BACK_SPACE: '\uE003', TAB: '\uE004', CLEAR: '\uE005', RETURN: '\uE006', ENTER: '\uE007', SHIFT: '\uE008', CONTROL: '\uE009', CTRL: '\uE009', ALT: '\uE00A', PAUSE: '\uE00B', ESCAPE: '\uE00C', SPACE: '\uE00D', PAGE_UP: '\uE00E', PAGE_DOWN: '\uE00F', END: '\uE010', HOME: '\uE011', ARROW_LEFT: '\uE012', LEFT: '\uE012', ARROW_UP: '\uE013', UP: '\uE013', ARROW_RIGHT: '\uE014', RIGHT: '\uE014', ARROW_DOWN: '\uE015', DOWN: '\uE015', INSERT: '\uE016', DELETE: '\uE017', SEMICOLON: '\uE018', EQUALS: '\uE019', NUMPAD0: '\uE01A', NUMPAD1: '\uE01B', NUMPAD2: '\uE01C', NUMPAD3: '\uE01D', NUMPAD4: '\uE01E', NUMPAD5: '\uE01F', NUMPAD6: '\uE020', NUMPAD7: '\uE021', NUMPAD8: '\uE022', NUMPAD9: '\uE023', MULTIPLY: '\uE024', ADD: '\uE025', SEPARATOR: '\uE026', SUBTRACT: '\uE027', DECIMAL: '\uE028', DIVIDE: '\uE029', F1: '\uE031', F2: '\uE032', F3: '\uE033', F4: '\uE034', F5: '\uE035', F6: '\uE036', F7: '\uE037', F8: '\uE038', F9: '\uE039', F10: '\uE03A', F11: '\uE03B', F12: '\uE03C', COMMAND: '\uE03D', META: '\uE03D' }; const mapCapabilities = { javascript: 'javascriptEnabled', cssselector: 'cssSelectorsEnabled', screenshot: 'takesScreenshot', storage: 'webStorageEnabled', alert: 'handlesAlerts', database: 'databaseEnabled', rotatable : 'rotatable' }; const Browser = PromiseClass.create({ /** * mouse buttons * @property MouseButtons * @public */ MouseButtons: MouseButtons, /** * keyboard buttons * @property Keys * @public */ Keys: Keys, /** * init browser instance * @method constructor * @private * @param {JWebDriver} driver JWebDriver instance * @param {String|Object} browserName or capabilitie object * @param {String} version * @param {String} platform * @param {Function} done callback function */ constructor(driver, browserName, version, platform){ let self = this; self._driver = driver; self.browserName = browserName; self.version = version; self.platform = platform; }, /** * init browser * @method init * @private * @param {Function} done callback function */ init(done){ let self = this; let driver = self._driver; let browserName = self.browserName; let version = self.version; let platform = self.platform; let sessionInfo = {}; if(typeof browserName === 'object'){ if(browserName.sessionId){ self.sessionId = browserName.sessionId; return done(); } sessionInfo = browserName; } else{ sessionInfo.browserName = browserName; if(version){ sessionInfo.version = version; } if(platform){ sessionInfo.platform = platform; } } browserName = sessionInfo.browserName; if(browserName && (browserName = mapBrowserNickName[browserName])){ sessionInfo.browserName = browserName; } if(sessionInfo.browserName){ sessionInfo = extend({}, defaultBrowserOptions, sessionInfo); } new Promise(function(resolve){ let hosts = sessionInfo.hosts; if(hosts){ // support hosts delete sessionInfo['hosts']; let config = { mode: 'hosts', hosts: hosts }; let hostsProxy = HostsProxy.createServer(config); hostsProxy.on('error', function(){}); hostsProxy.listen(0, function(msg){ let localIp = getLocalIP(); let proxyHost = localIp+':'+msg.port; sessionInfo.proxy = { 'proxyType': 'manual', 'httpProxy': proxyHost, 'sslProxy': proxyHost }; self._hostsProxy = hostsProxy; resolve(sessionInfo); }); } else{ resolve(sessionInfo); } }).then(function (sessionInfo) { if (sessionInfo.browserName === 'chrome' && !sessionInfo.chromeOptions) { sessionInfo = { ...sessionInfo, chromeOptions: { args: ['--enable-automation', '--incognito'], }, } } if (sessionInfo.browserName !== 'chrome' && sessionInfo.chromeOptions) { delete sessionInfo.chromeOptions; } let sessionOptions = { 'desiredCapabilities': sessionInfo }; driver.execCmd('newSession', {}, sessionOptions, function(error, ret){ if(error){ done(error); } else{ self.sessionId = ret.sessionId; let capabilities = ret.value; self.capabilities = capabilities; self.browserMode = capabilities['browserName'] ? true : false; self.mobileMode = !self.browserMode; if(self.browserMode){ self.browserName = capabilities['browserName'] || self.browserName; self.version = capabilities['version'] || self.version; self.platform = capabilities['platform'] || self.platform; } done(); } }).catch(done); }).catch(done); }, /** * add log * @method addLog * @public * @param {COMMAND|DATA|RESULT|ERROR|WARNING|INFO} type log type * @param {Object} message log message */ addLog(type, message){ this._driver.addLog(type, message); }, /** * execute protocal command with this session * @method execCmd * @public * @param {String} cmd protocal command, defined in command.js * @param {Object} [pathData] replace the path parameters, no need to add sessionId * @param {Object} [data] send data to protocal api * @param {Function} done callback function * @return {Object} the return object from webdriver server */ execCmd(cmd, pathData, data, done){ let self = this; if(typeof pathData === 'function'){ pathData = undefined; data = undefined; } else if(typeof data === 'function'){ data = undefined; } done = getDone(arguments); pathData = pathData || {}; pathData.sessionId = self.sessionId; self._driver.execCmd(cmd, pathData, data, done); }, /** * get browser info * @method info * @public * @return {Object} info object */ info(){ return this.capabilities; }, /** * get webdriver logs * @method logs * @public * @param {String} type * @return {Array} logs */ logs(type, done){ this.execCmd('getLogs', {}, {type: type}, function(error, ret){ done(error, ret && ret.value); }); }, /** * get webdriver log types * @method logTypes * @public * @return {Array} typs */ logTypes(done){ this.execCmd('getLogTypes', function(error, ret){ done(error, ret && ret.value); }); }, /** * get capability support * @method support * @public * @param {String} capability * @return {Object} info object */ support(capability){ let capabilities = this.capabilities; let rawCapability = mapCapabilities[capability.toLowerCase()]; return rawCapability && capabilities[rawCapability] || false; }, /** * sleep sync * @method sleep * @public * @param {Number} ms millisecond * @param {Function} done callback function */ sleep(ms, done){ this._driver.sleep(ms, done); }, /** * config webdriver options * @method config * @public * @param {String} options options for webdriver config * @param {Function} done callback function */ config(options, done){ let self = this; // timeout let pageloadTimeout = options.pageloadTimeout; let scriptTimeout = options.scriptTimeout; let asyncScriptTimeout = options.asyncScriptTimeout; let implicitTimeout = options.implicitTimeout; Promise.resolve().then(function(){ if(pageloadTimeout){ return self.execCmd('configTimeouts', {}, { 'type': 'page load', 'ms': pageloadTimeout }); } }).then(function(){ if(scriptTimeout){ return self.execCmd('configTimeouts', {}, { 'type': 'script', 'ms': scriptTimeout }); } }).then(function(){ if(asyncScriptTimeout){ return self.execCmd('configAsyncScriptTimeout', {}, { 'ms': asyncScriptTimeout }); } }).then(function(){ if(implicitTimeout){ return self.execCmd('configImplicitTimeout', {}, { 'ms': implicitTimeout }); } }).then(function(){ done(); }).catch(done); }, /** * goto url or get url * @method url * @public * @param {String} [url] url for goto * @param {Function} done callback function * @return {url|this} return this or url */ url(url, done){ let self = this; if(typeof url === 'function'){ url = undefined; } done = getDone(arguments); // goto url if(url){ self.execCmd('setUrl', {}, { 'url': url }, done); } // get url else{ self.execCmd('getUrl', function(error, ret){ done(error, ret && ret.value); }); } }, /** * execute script in browser * @method exec * @public * @param {String|Function} script function to exec * @param {Array} [args] arguments send to script * @param {Function} done callback function * @return {any} js function returned value */ exec(script, args, done){ let self = this; if(typeof args === 'function'){ args = undefined; } done = getDone(arguments); args = args !== undefined ? args : []; if(args instanceof Array === false){ args = Array.prototype.slice.call(arguments); script = args.shift(); args.pop(); } if(typeof script === 'string' || typeof script === 'function'){ let isAsync = /(\(|,)\s*(done|callback|cb)\)/i.test(script); script = String(script); // support for: return document.title; if(/^\s*function\s*\(/.test(script) === false){ script = 'function(){'+script+'}'; } script = 'return (' + script + ').apply(null, arguments);'; script = script.replace(/\n/g, ''); args = args.map(function(arg){ if(arg instanceof Elements){ arg.toJSON(function(error, json){ arg = json; }); } return arg; }); self.execCmd(isAsync?'execASync':'exec', {}, { script: script, args: args }, function(error, ret){ done(error?'exec timeout':null, ret && ret.value); }); } else{ done('First param must be string or function'); } }, /** * execute script in browser * @method eval * @public * @param {String|Function} script function to eval * @param {Array} [args] arguments send to script * @param {Function} done callback function * @return {any} js function returned value */ eval(script, args, done){ //jshint unused:false this.exec.apply(this, arguments); }, /** * get current window handle * @method windowHandle * @public * @param {Boolean} useCache * @param {Function} done callback function * @return {String} window handle */ windowHandle(useCache, done){ let self = this; if(typeof useCache === 'function'){ useCache = undefined; } done = getDone(arguments); let windowHandle = self._windowHandle; if(useCache && windowHandle){ done(null, windowHandle); } else if(self.browserMode){ self.execCmd('getCurrentWindowHandle', function(error, ret){ if(ret){ self._windowHandle = ret.value; } done(error, ret && ret.value); }); } else{ self._windowHandle = 'current'; done(null, self._windowHandle); } }, /** * get all window handles * @method windowHandles * @public * @param {Function} done callback function * @return {Array} window handle */ windowHandles(done){ this.execCmd('getAllWindowHandles', function(error, ret){ done(error, ret && ret.value); }); }, /** * switch to window * @method switchWindow * @public * @param {String} windowHandle window handle * @param {Function} done callback function */ switchWindow(windowHandle, done){ let self = this; new Promise(function(resolve){ if(typeof windowHandle === 'number'){ self.windowHandles(function(error, ret){ windowHandle = ret[windowHandle]; if(windowHandle === undefined){ done('Window index overflow'); } else{ resolve(windowHandle); } }); } else{ resolve(windowHandle); } }).then(function(windowHandle){ self.execCmd('switchWindow', {}, { name: windowHandle }, function(error, ret){ self._windowHandle = windowHandle; done(error, ret); }); }); }, /** * open new window * @method newWindow * @public * @param {String} windowHandle window handle * @param {Function} done callback function * @return {String} return window handle */ newWindow(url, name, features, done){ let self = this; if(typeof name === 'function'){ name = undefined; features = undefined; } else if(typeof features === 'function'){ features = undefined; } done = getDone(arguments); let script = 'function(url, name, features){window.open(url, name, features);}'; self.exec(script, url, name, features).then(function(){ return self.windowHandles(); }).then(function(arrWindowHandles){ done(null, arrWindowHandles[arrWindowHandles.length-1]); }).catch(done); }, /** * get all frame elements * @method frames * @public * @param {Function} done callback function * @return {Element} */ frames(done){ this.find('//iframe', done); }, /** * switch to frame or main page * @method switchFrame * @public * @param {Elements|String} frame * @param {Function} done callback function */ switchFrame(frame, done){ let self = this; new Promise(function(resolve){ if(typeof frame === 'string'){ resolve(self.find(frame)); } else{ resolve(frame); } }).then(function(frame){ if(frame instanceof Elements){ return frame.toJSON(true); } else{ return frame; } }).then(function(frame){ self.execCmd('switchFrame', {}, { id : frame }, done); }); }, /** * switch to parent frame * @method switchFrameParent * @public * @param {Function} done callback function */ switchFrameParent(done){ this.execCmd('switchFrameParent', done); }, /** * close current window * @method closeWindow * @public * @param {Function} done callback function */ closeWindow(done){ this.execCmd('closeWindow', done); }, /** * get or set position of window * @method position * @public * @param {Number|Object} [x] x position * @param {Number|Object} [x] x position * @param {Function} done callback function * @return {Object|this} return {x:1,y:1} or this */ position(x, y, done){ let self = this; if(typeof x === 'function'){ x = undefined; y = undefined; } else if(typeof y === 'function'){ y = undefined; } done = getDone(arguments); Promise.resolve(self.windowHandle(true)).then(function(windowHandle){ if(x === undefined){ return self.execCmd('getWindowPosition', { windowHandle: windowHandle }, function(error, ret){ done(error, ret && ret.value); }); } else if(x.x !== undefined){ y = x.y; x = x.x; } self.execCmd('setWindowPosition', { windowHandle: windowHandle }, { x: x, y: y }, done); }).catch(done); }, /** * get or set size of window * @method size * @public * @param {Number|Object} [width] * @param {Number|Object} [height] * @param {Function} done callback function * @return {Object|this} return {width:1,height:1} or this */ size(width, height, done){ let self = this; if(typeof width === 'function'){ width = undefined; height = undefined; } else if(typeof height === 'function'){ height = undefined; } done = getDone(arguments); Promise.resolve(self.windowHandle(true)).then(function(windowHandle){ if(width === undefined){ return self.execCmd('getWindowSize', { windowHandle: windowHandle }, function(error, ret){ done(error, ret && ret.value); }); } else if(width.width !== undefined){ height = width.height; width = width.width; } self.execCmd('setWindowSize', { windowHandle: windowHandle }, { width: width, height: height }, done); }).catch(done); }, /** * maximize the window * @method maximize * @public * @param {Function} done callback function */ maximize(done){ let self = this; Promise.resolve(self.windowHandle(true)).then(function(windowHandle){ self.execCmd('maximizeWindow', { windowHandle : windowHandle }, done); }).catch(done); }, /** * get screenshot * @method getScreenshot * @public * @param {Object} [options] options for screenshot * @param {Function} done callback function * @return {String} return screenshot by base64 format */ getScreenshot(options, done){ let self = this; if(typeof options === 'function'){ options = {}; } else{ if(typeof options === 'string'){ options = { filename: options }; } } done = getDone(arguments); let filename = options.filename; let elem = options.elem; self.execCmd('getScreenshot', function*(error, ret){ let png64 = ret && ret.value; if(png64){ if(elem){ let elems = yield self.wait(elem, 1000); if(elems.length === 1){ let gmShot = gm(new Buffer(png64, 'base64')).quality(100); let rect, x, y, width, height; if(self.browserMode){ rect = yield self.eval(function(elems){ var rect = elems[0].getBoundingClientRect(); rect.devicePixelRatio = window.devicePixelRatio; return rect; }, elems); let devicePixelRatio = rect.devicePixelRatio || 1; x = rect.left * devicePixelRatio; y = rect.top * devicePixelRatio; width = rect.width * devicePixelRatio; height = rect.height * devicePixelRatio; } else{ rect = yield elems.rect(); x = rect.x; y = rect.y; width = rect.width; height = rect.height; } gmShot.crop(width, height, x, y); png64 = yield new Promise((resolve) => gmShot.toBuffer('PNG', (error, buffer) => resolve(buffer))); png64 =png64.toString('base64'); } } if(filename){ fs.writeFileSync(filename, png64, 'base64'); } } done(error, png64); }).catch(done); }, /** * get title * @method title * @public * @param {Function} done callback function * @return {String} return document title */ title(done){ this.execCmd('getTitle', function(error, ret){ done(error, ret && ret.value); }); }, /** * get source * @method source * @public * @param {Function} done callback function * @return {String} return source */ source(done){ this.execCmd('getSource', function(error, ret){ done(error, ret && ret.value); }); }, /** * nick name of source * @method html * @public * @param {Function} done callback function * @return {String} return document html */ html(done){ this.execCmd('getSource', function(error, ret){ done(error, ret && ret.value); }); }, /** * refresh the window * @method refresh * @public * @param {Function} done callback function */ refresh(done){ this.execCmd('setRefresh', done); }, /** * back the window * @method back * @public * @param {Function} done callback function */ back(done){ this.execCmd('setBack', done); }, /** * forward the window * @method forward * @public * @param {Function} done callback function */ forward(done){ this.execCmd('setForward', done); }, /** * get all cookies * @method cookies * @public * @param {Boolean} isArray * @param {Function} done callback function * @return {Array} return all cookies */ cookies(isArray, done){ if(typeof isArray === 'function'){ isArray = undefined; } done = getDone(arguments); this.execCmd('getAllCookies', function(error, ret){ if(error){ done(error); } else{ let arrCookies = ret.value; if(isArray){ done(null, arrCookies); } else{ let mapCookies = {}; arrCookies.forEach(function(cookie){ mapCookies[cookie.name] = cookie; }); done(null, mapCookies); } } }); }, /** * get or set cookie * @method cookie * @public * @param {String} name cookie name * @param {String} [value] cookie value * @param {Object} [options] options for cookie * @param {Function} done callback function * @return {String} return cookie value or this */ cookie(name, value, options, done){ let self = this; if(typeof value === 'function'){ value = undefined; options = undefined; } else if(typeof options === 'function'){ options = undefined; } done = getDone(arguments); let cookieInfo; if(value === undefined){ self.cookies(function(error, ret){ if(error){ done(error); } else{ cookieInfo = ret[name]; done(null, cookieInfo && cookieInfo.value); } }); } else{ cookieInfo = options || {}; cookieInfo.name = String(name); cookieInfo.value = String(value); let expiry = cookieInfo.expiry; if(expiry && typeof expiry === 'string'){ let match = expiry.match(/^(\d+)\s*(second|minute|hour|day|month|year)s?$/); if(match !== null){ let number = parseInt(match[1], 10); let unit = match[2]; let date = new Date(); switch(unit){ case 'second': date.setSeconds(date.getSeconds() + number); break; case 'minute': date.setMinutes(date.getMinutes() + number); break; case 'hour': date.setHours(date.getHours() + number); break; case 'day': date.setDate(date.getDate() + number); break; case 'month': date.setMonth(date.getMonth() + number); break; case 'year': date.setFullYear(date.getFullYear() + number); break; } cookieInfo.expiry = date.getTime() / 1000; } else{ done('Bad format for expiry: ' + expiry); } } self.execCmd('setCookie', {}, { cookie: cookieInfo }, done); } }, /** * remove cookie * @method removeCookie * @public * @param {String} name cookie name * @param {Function} done callback function */ removeCookie(name, done){ this.execCmd('delCookie', { name: name }, done); }, /** * clear all cookies * @method clearCookies * @public * @param {Function} done callback function */ clearCookies(done){ this.execCmd('clearAllCookies', done); }, /** * get all local storage keys * @method localStorageKeys * @public * @param {Function} done callback function * @return {Array} return all local storage keys */ localStorageKeys(done){ this.execCmd('getAllLocalStorageKeys', function(error, ret){ done(error, ret && ret.value); }); }, /** * get or set local storage * @method localStorage * @public * @param {String} name local storage name * @param {String} [value] local storage value * @param {Function} done callback function * @return {String} return local storage value or this */ localStorage(name, value, done){ let self = this; if(typeof value === 'function'){ value = undefined; } done = getDone(arguments); if(value !== undefined){ self.execCmd('setLocalStorage', {}, { key: String(name), value: String(value) }, done); } else{ self.execCmd('getLocalStorage', { key: String(name) }, function(error, ret){ done(error, ret && ret.value); }); } }, /** * remove local storage * @method removeLocalStorage * @public * @param {String} name local storage name * @param {Function} done callback function */ removeLocalStorage(name, done){ this.execCmd('deleteLocalStorage', { key: name }, done); }, /** * clear all local storage * @method clearLocalStorages * @public * @param {Function} done callback function */ clearLocalStorages(done){ this.execCmd('clearAllLocalStorages', done); }, /** * get all session storage keys * @method sessionStorageKeys * @public * @param {Function} done callback function * @return {Array} return all session storage keys */ sessionStorageKeys(done){ this.execCmd('getAllSessionStorageKeys', function(error, ret){ done(error, ret && ret.value); }); }, /** * get or set session storage * @method sessionStorage * @public * @param {String} name session storage name * @param {String} [value] session storage value * @param {Function} done callback function * @return {String} return session storage value or this */ sessionStorage(name, value, done){ let self = this; if(typeof value === 'function'){ value = undefined; } done = getDone(arguments); if(value !== undefined){ self.execCmd('setSessionStorage', {}, { key: String(name), value: String(value) }, done); } else{ self.execCmd('getSessionStorage', { key: String(name) }, function(error, ret){ done(error, ret && ret.value); }); } }, /** * remove session storage * @method removeSessionStorage * @public * @param {String} name session storage name * @param {Function} done callback function */ removeSessionStorage(name, done){ this.execCmd('deleteSessionStorage', { key: name }, done); }, /** * clear all session storage * @method clearSessionStorages * @public * @param {Function} done callback function */ clearSessionStorages(done){ this.execCmd('clearAllSessionStorages', done); }, /** * get alert text * @method getAlert * @public * @param {Function} done callback function * @return {String} */ getAlert(done){ this.execCmd('getAlert', function(error, ret){ done(error, ret && ret.value); }); }, /** * accept alert or confirm * @method acceptAlert * @public * @param {Function} done callback function */ acceptAlert(done){ this.execCmd('acceptAlert', done); }, /** * dismiss confirm * @method dismissAlert * @public * @param {Function} done callback function */ dismissAlert(done){ this.execCmd('dismissAlert', done); }, /** * set text to prompt * @method setAlert * @public * @param {String} text * @param {Function} done callback function */ setAlert(text, done){ this.execCmd('setAlert', {}, { text: text }, done); }, /** * send keys to browser * @method sendKeys * @public * @param {String} text * @param {Function} done callback function */ sendKeys(text, done){ let self = this; let Keys = self.Keys; text = text.replace(/{(\w+)}/g, function(all, name){ let key = Keys[name.toUpperCase()]; return key?key:all; }); const value = text.split(''); self.execCmd('sendKeys', {}, { value, }, done); }, /** * check sticky key * @method isStickyKey * @public * @param {String} key * @return {Boolean} */ isStickyKey(key){ let Keys = this.Keys; return key === Keys.CTRL || key === Keys.SHIFT || key === Keys.ALT || key === Keys.META; }, /** * send keydown * @method keyDown * @public * @param {String} key * @param {Function} done callback function */ keyDown(key, done){ let self = this; let Keys = self.Keys; key = Keys[key.toUpperCase()] || key; if(self.isStickyKey(key)){ self.sendKeys(key, done); } else{ done('keyDown or keyUp only support: CTRL|SHIFT|ALT|COMMAND|META'); } }, /** * send keyup * @method keyUp * @public * @param {String} key * @param {Function} done callback function */ keyUp(key, done){ let self = this; let Keys = self.Keys; key = Keys[key.toUpperCase()] || key; if(self.isStickyKey(key)){ self.sendKeys(key, done); } else{ done('keyDown or keyUp only support: CTRL|SHIFT|ALT|COMMAND|META'); } }, /** * send mousemove (first element) * @method mouseMove * @public * @param {Elements|String} elements * @param {Number|Object} [x] * @param {Number} [y] * @param {Function} done callback function */ mouseMove(elements, x, y, done){ let self = this; if(typeof x === 'function'){ x = undefined; y = undefined; } else if(typeof y === 'function'){ y = undefined; } done = getDone(arguments); new Promise(function(resolve){ if(typeof elements === 'string'){ resolve(self.find(elements)); } else{ resolve(elements); } }).then(function(elements){ let data = {}; if(elements.length > 0){ data.element = elements.elementIds[0]; } else{ return done('Elements empty: '+ elements.value+' ('+elements.using+')'); } if(x !== undefined && x.x !== undefined){ y = x.y; x = x.x; } if(x !== undefined && y !== undefined){ data.xoffset = x; data.yoffset = y; } self.execCmd('mouseMove', {}, data, done); }).catch(done); }, /** * send mousedown * @method mouseDown * @public * @param {String} [key] * @param {Function} done callback function */ mouseDown(key, done){ let self = this; if(typeof key === 'function'){ key = undefined; } done = getDone(arguments); if(typeof key === 'string'){ let MouseButtons = self.MouseButtons; key = key.toUpperCase(); key = MouseButtons[key] || 0; } self.execCmd('mouseDown', {}, { button: key || 0 }, done); }, /** * send mouseup * @method mouseUp * @public * @param {String} [key] * @param {Function} done callback function */ mouseUp(key, done){ let self = this; if(typeof key === 'function'){ key = undefined; } done = getDone(arguments); if(typeof key === 'string'){ let MouseButtons = self.MouseButtons; key = key.toUpperCase(); key = MouseButtons[key] || 0; } self.execCmd('mouseUp', {}, { button: key || 0 }, done); }, /** * send click * @method click * @public * @param {String} [key] * @param {Function} done callback function */ click(key, done){ let self = this; if(typeof key === 'function'){ key = undefined; } done = getDone(arguments); if(typeof key === 'string'){ let MouseButtons = self.MouseButtons; key = key.toUpperCase(); key = MouseButtons[key] || 0; } self.execCmd('click', {}, { button: key || 0 }, done); }, /** * send double Click * @method dblClick * @public * @param {Function} done callback function */ dblClick(done){ this.execCmd('doubleClick', done); }, /** * send double Click * @method doubleClick * @public * @param {Function} done callback function */ doubleClick(done){ this.execCmd('doubleClick', done); }, /** * drag one element to another * @method dragDrop * @public * @param {Elements} from * @param {Elements} to * @param {Function} done callback function */ dragDrop(from, to, done){ let self = this; let fromOffset; if(from.selector){ fromOffset = { x: from.x, y: from.y }; from = from.selector; } let toOffset; if(to.selector){ toOffset = { x: to.x, y: to.y }; to = to.selector; } self.mouseMove(from, fromOffset).mouseDown().mouseMove(to, toOffset).mouseUp().then(function(){ done(); }).catch(done); }, /** * find element * @method find * @public * @param {String} [using] find mode: class name|css selector|id|name|link text|partial link text|tag name|xpath * @param {String} value find pattern * @param {Function} done callback function * @return {Elments} */ find(using, value, done){ if(typeof value === 'function'){ value = undefined; } done = getDone(arguments); let elements = new Elements(this, using, value); elements.init(function(error){ done(error, elements); }).catch(done); }, /** * find visible element * @method findVisible * @public * @param {String} [using] find mode: class name|css selector|id|name|link text|partial link text|tag name|xpath * @param {String} value find pattern * @param {Function} done callback function * @return {Elments} */ findVisible(using, value, done){ let self = this; if(typeof value === 'function'){ value = undefined; } done = getDone(arguments); self.find(using, value, function*(error, elements){ if(elements.length > 0){ let json = yield elements.toJSON(); // filter visible element let newJson = []; try{ newJson = yield self.exec('function(arg1){\ function curCSS(elem, name){\ var curStyle = elem.currentStyle;\ var style = elem.style;\ return (curStyle && curStyle[name]) || (style && style[name]);\ }\ function isHidden(elem){\ return ( elem.offsetWidth === 0 && elem.offsetHeight === 0 ) || (curCSS( elem, "display" ) === "none");\ }\ var elements = arg1.elements;\ var newElements = [], element;\ for(var i in elements){\ element = elements[i];\ if(!isHidden(element)){\ newElements.push(element);\ }\ }\ return newElements;\ }', {elements: json}); } catch(e){} if(newJson.length === 0){ let message = 'Find visible elements failed'; self.addLog('ERROR', message); done(message); } else{ let elementIds = []; newJson.forEach(function(element){ elementIds.push(element.ELEMENT); }); elements.elementIds = elementIds; elements.length = elementIds.length; done(null, elements); } } else{ done(error); } }).catch(done); }, /** * wait for element displayed or removed * @method wait * @public * @param {String} [using] find mode: class name|css selector|id|name|link text|partial link text|tag name|xpath * @param {String} value find pattern * @param {String} [options] * @param {Function} done callback function * @return {Elments} */ wait(using, value, options, done){ let self = this; if(typeof options === 'function'){ if(typeof value === 'string'){ // using, value, done options = undefined; } else{ // value, options, done options = value; value = using; using = undefined; } } else if(typeof value === 'function'){ // value, done value = using; using = undefined; options = undefined; } done = getDone(arguments); if(typeof options === 'number'){ options = { timeout: options }; } options = extend({}, { timeout: 10000, displayed: true, removed: false }, options); let timeout = options.timeout; let removed = options.removed; let displayed = removed ? false : options.displayed; let noerror = options.noerror; let startTime = new Date().getTime(); let _timer = null; waitElement(); function endWait(error, result) { clearInterval(_timer); done(error, result); } function waitElement(){ self[(!self.mobileMode && displayed)?'findVisible':'find'](using, value, function(error, elements){ if(elements){ if(removed === true && elements.length === 0){ // removed return endWait(null, elements); } else if(removed === false && elements.length > 0){ if(displayed){ // displayed return endWait(null, elements); } else{ // wait dom return endWait(null, elements); } } } // timeout if(new Date().getTime() - startTime > timeout){ let message = 'Wait element '+(removed?'removed':'displayed')+' timeout: '+(using?(using+' ,'):'')+value+' ,' + timeout + 'ms'; self.addLog('ERROR', message); if(noerror){ endWait(null, []); } else{ endWait(message); } } else{ _timer = setTimeout(waitElement, 500); } }); } }, /** * scroll to * @method scrollTo * @public * @param {Elements|String} [elements] * @param {Number} [x] * @param {Number} [y] * @param {Function} done callback function */ scrollTo(elements, x, y, done){ let self = this; if(typeof x === 'function'){ x = undefined; y = undefined; } else if(typeof y === 'function'){ y = undefined; } done = getDone(arguments); new Promise(function(resolve){ if(typeof elements === 'string'){ resolve(self.find(elements)); } else{ resolve(elements); } }).then(function(elements){ if(elements instanceof Elements){ return new Promise(function(resolve){ elements.offset(function(error, offset){ if(x !== undefined && x.x !== undefined){ y = x.y; x = x.x; } offset.x += x || 0; offset.y += y || 0; x = offset.x; y = offset.y; resolve({ x: x, y: y }); }); }); } else{ // no elements y = x; x = elements; if(x !== undefined && x.x !== undefined){ y = x.y; x = x.x; } return { x: x, y: y }; } }).then(function(offset){ let script = 'function(x, y){window.scrollTo(x, y);}'; self.exec(script, offset.x, offset.y, done); }).catch(done); }, /** * upload file to browser machine * @method uploadFileToServer * @public * @param {String} localPath * @param {Function} done callback function * @return {String} webdriver server filepath */ uploadFileToServer(localPath, done){ let self = this; if(fs.existsSync(localPath)){ let filedata = fs.readFileSync(localPath); let zip = new Zip(); zip.file(path.basename(localPath), filedata); let base64zip = zip.generate({base64:true, compression:'DEFLATE'}); self.execCmd('uploadFile', {}, { file: base64zip }, function(error, ret){ done(error, ret && ret.value); }); } else{ done('localPath doesn\'t exist'); } }, /** * touch down * @method touchDown * @public * @param {Number} x * @param {Number} y * @param {Function} done callback function */ touchDown(x, y, done){ let self = this; if(typeof x === 'function'){ x = undefined; y = undefined; } else if(typeof y === 'function'){ y = undefined; } done = getDone(arguments); if(x !== undefined && x.x !== undefined){ y = x.y; x = x.x; } self.execCmd('touchDown', {}, { x: x, y: y }, done); }, /** * touch move * @method touchMove * @public * @param {Number} x * @param {Number} y * @param {Function} done callback function */ touchMove(x, y, done){ let self = this; if(typeof x === 'function'){ x = undefined; y = undefined; } else if(typeof