| 1 | | /** |
| 2 | | * This class is used for control browser. |
| 3 | | * <p> |
| 4 | | * Browser类用来实例化一个对象,以绑定目标浏览器并调用接口进行控制。 |
| 5 | | * </p> |
| 6 | | * @class Browser |
| 7 | | */ |
| 8 | | |
| 9 | 1 | var http = require('http'), |
| 10 | | fs = require('fs'), |
| 11 | | Fiber = require('fibers'), |
| 12 | | extend = require('xtend'); |
| 13 | | |
| 14 | 1 | var Element = require('./element'); |
| 15 | | |
| 16 | 1 | var arrCommands = require('./commands.js'); |
| 17 | | |
| 18 | | //颜色列表 |
| 19 | 1 | var colors = { |
| 20 | | black: '\x1b[0;30m', |
| 21 | | dkgray: '\x1b[1;30m', |
| 22 | | brick: '\x1b[0;31m', |
| 23 | | red: '\x1b[1;31m', |
| 24 | | green: '\x1b[0;32m', |
| 25 | | lime: '\x1b[1;32m', |
| 26 | | brown: '\x1b[0;33m', |
| 27 | | yellow: '\x1b[1;33m', |
| 28 | | navy: '\x1b[0;34m', |
| 29 | | blue: '\x1b[1;34m', |
| 30 | | violet: '\x1b[0;35m', |
| 31 | | magenta: '\x1b[1;35m', |
| 32 | | teal: '\x1b[0;36m', |
| 33 | | cyan: '\x1b[1;36m', |
| 34 | | ltgray: '\x1b[0;37m', |
| 35 | | white: '\x1b[1;37m', |
| 36 | | reset: '\x1b[0m' |
| 37 | | }; |
| 38 | | |
| 39 | | //响应代码 |
| 40 | 1 | var responseCodes = { |
| 41 | | '0': {type: 'Success', message: 'The command executed successfully.'}, |
| 42 | | '7': {type: 'NoSuchElement', message: 'An element could not be located on the page using the given search parameters.'}, |
| 43 | | '8': {type: 'NoSuchFrame', message: 'A request to switch to a frame could not be satisfied because the frame could not be found.'}, |
| 44 | | '9': {type: 'UnknownCommand', message: 'The requested resource could not be found, or a request was received using an HTTP method that is not supported by the mapped resource.'}, |
| 45 | | '10': {type: 'StaleElementReference', message: 'An element command failed because the referenced element is no longer attached to the DOM.'}, |
| 46 | | '11': {type: 'ElementNotVisible', message: 'An element command could not be completed because the element is not visible on the page.'}, |
| 47 | | '12': {type: 'InvalidElementState', message: 'An element command could not be completed because the element is in an invalid state (e.g. attempting to click a disabled element).'}, |
| 48 | | '13': {type: 'UnknownError', message: 'An unknown server-side error occurred while processing the command.'}, |
| 49 | | '15': {type: 'ElementIsNotSelectable', message: 'An attempt was made to select an element that cannot be selected.'}, |
| 50 | | '17': {type: 'JavaScriptError', message: 'An error occurred while executing user supplied JavaScript.'}, |
| 51 | | '19': {type: 'XPathLookupError', message: 'An error occurred while searching for an element by XPath.'}, |
| 52 | | '23': {type: 'NoSuchWindow', message: 'A request to switch to a different window could not be satisfied because the window could not be found.'}, |
| 53 | | '24': {type: 'InvalidCookieDomain', message: 'An illegal attempt was made to set a cookie under a different domain than the current page.'}, |
| 54 | | '25': {type: 'UnableToSetCookie', message: 'A request to set a cookie\'s value could not be satisfied.'}, |
| 55 | | '26': {type: 'UnexpectedAlertOpen', message: 'A modal dia` was open, blocking this operation'}, |
| 56 | | '27': {type: 'NoAlertOpenError', message: 'An attempt was made to operate on a modal dialog when one was not open.'}, |
| 57 | | '28': {type: 'ScriptTimeout', message: 'A script did not complete before its timeout expired.'}, |
| 58 | | '29': {type: 'InvalidElementCoordinates', message: 'The coordinates provided to an interactions operation are invalid.'}, |
| 59 | | '30': {type: 'IMENotAvailable', message: 'IME was not available.'}, |
| 60 | | '31': {type : 'IMEEngineActivationFailed', message: 'An IME engine could not be started.'}, |
| 61 | | '32': {type: 'InvalidSelector', message: 'Argument was an invalid selector (e.g. XPath/CSS).'} |
| 62 | | }; |
| 63 | | |
| 64 | | //默认浏览器参数 |
| 65 | 1 | var defaultOptions = { |
| 66 | | 'browserName': 'firefox', |
| 67 | | 'version': '', |
| 68 | | 'platform': 'ANY' |
| 69 | | } |
| 70 | | |
| 71 | | //浏览器昵称 |
| 72 | 1 | var arrBrowserNickName = { |
| 73 | | 'ie': 'internet explorer', |
| 74 | | 'ff': 'firefox' |
| 75 | | } |
| 76 | | |
| 77 | | //鼠标按钮映射表 |
| 78 | 1 | var arrMouseButton = { |
| 79 | | 'left': 0, |
| 80 | | 'middle': 1, |
| 81 | | 'right': 2 |
| 82 | | } |
| 83 | | |
| 84 | | //键盘特殊按键映射表 |
| 85 | 1 | var arrSpecKeys = { |
| 86 | | 'null': '\uE000', |
| 87 | | 'cancel': '\uE001', |
| 88 | | 'help': '\uE002', |
| 89 | | 'back': '\uE003', |
| 90 | | 'tab': '\uE004', |
| 91 | | 'clear': '\uE005', |
| 92 | | 'return': '\uE006', |
| 93 | | 'enter': '\uE007', |
| 94 | | 'shift': '\uE008', |
| 95 | | 'ctrl': '\uE009', |
| 96 | | 'alt': '\uE00A', |
| 97 | | 'pause': '\uE00B', |
| 98 | | 'esc': '\uE00C', |
| 99 | | 'space': '\uE00D', |
| 100 | | 'pageup': '\uE00E', |
| 101 | | 'pagedown': '\uE00F', |
| 102 | | 'end': '\uE010', |
| 103 | | 'home': '\uE011', |
| 104 | | 'left': '\uE012', |
| 105 | | 'up': '\uE013', |
| 106 | | 'right': '\uE014', |
| 107 | | 'down': '\uE015', |
| 108 | | 'insert': '\uE016', |
| 109 | | 'delete': '\uE017', |
| 110 | | 'semicolon': '\uE018', |
| 111 | | 'equals': '\uE019', |
| 112 | | 'num0': '\uE01A', |
| 113 | | 'num1': '\uE01B', |
| 114 | | 'num2': '\uE01C', |
| 115 | | 'num3': '\uE01D', |
| 116 | | 'num4': '\uE01E', |
| 117 | | 'num5': '\uE01F', |
| 118 | | 'num6': '\uE020', |
| 119 | | 'num7': '\uE021', |
| 120 | | 'num8': '\uE022', |
| 121 | | 'num9': '\uE023', |
| 122 | | 'multiply': '\uE024', |
| 123 | | 'add': '\uE025', |
| 124 | | 'separator': '\uE026', |
| 125 | | 'subtract': '\uE027', |
| 126 | | 'decimal': '\uE028', |
| 127 | | 'divide': '\uE029', |
| 128 | | 'f1': '\uE031', |
| 129 | | 'f2': '\uE032', |
| 130 | | 'f3': '\uE033', |
| 131 | | 'f4': '\uE034', |
| 132 | | 'f5': '\uE035', |
| 133 | | 'f6': '\uE036', |
| 134 | | 'f7': '\uE037', |
| 135 | | 'f8': '\uE038', |
| 136 | | 'f9': '\uE039', |
| 137 | | 'f10': '\uE03A', |
| 138 | | 'f11': '\uE03B', |
| 139 | | 'f12': '\uE03C', |
| 140 | | 'command': '\uE03D', |
| 141 | | 'meta': '\uE03D' |
| 142 | | } |
| 143 | | |
| 144 | 1 | var Browser = function(){ |
| 145 | 4 | var self = this; |
| 146 | 4 | return self._init.apply(self, arguments); |
| 147 | | } |
| 148 | | |
| 149 | 1 | Browser.prototype = { |
| 150 | | |
| 151 | | /** |
| 152 | | * 初始化浏览器对象 |
| 153 | | * @method _init |
| 154 | | * @private |
| 155 | | * @param {Object} config jWebDriver config |
| 156 | | * @param {Object} options 浏览器初始化参数 |
| 157 | | * @param {Function} callback 回调函数 |
| 158 | | */ |
| 159 | | _init: function(config, options, callback){ |
| 160 | 4 | var self = this; |
| 161 | 4 | self.sessionId = null; |
| 162 | 4 | self.logMode = config.logMode; |
| 163 | 4 | self.host = config.host; |
| 164 | 4 | self.port = config.port; |
| 165 | 4 | var browserName = options.browserName; |
| 166 | 4 | if(browserName && (browserName = arrBrowserNickName[browserName])){ |
| 167 | 0 | options.browserName = browserName; |
| 168 | | } |
| 169 | 4 | var sessionOptions = { |
| 170 | | 'desiredCapabilities': extend({}, defaultOptions, options) |
| 171 | | }; |
| 172 | 4 | self.doCommand('setSession', sessionOptions, callback); |
| 173 | 4 | self.windowHandle = 'current'; |
| 174 | | }, |
| 175 | | |
| 176 | | /** |
| 177 | | * 输出日志 |
| 178 | | * @method log |
| 179 | | * @public |
| 180 | | * @param {String} type 日志类型 |
| 181 | | * @param {String} message 日志内容 |
| 182 | | * @return {Browser} 当前Browser对象实例 |
| 183 | | */ |
| 184 | | log: function(type, message){ |
| 185 | 738 | var self = this; |
| 186 | 738 | if(message === undefined){ |
| 187 | 0 | message = type; |
| 188 | 0 | type = 'INFO'; |
| 189 | | } |
| 190 | 738 | if(self.logMode === 'all' || (self.logMode === 'error' && type === 'ERROR')){ |
| 191 | 0 | var dateString = (new Date()).toString().match(/\d\d:\d\d:\d\d/)[0]; |
| 192 | 0 | var mapColors = { |
| 193 | | 'COMMAND': colors.violet, |
| 194 | | 'DATA': colors.brown, |
| 195 | | 'RESULT': colors.teal, |
| 196 | | 'ERROR': colors.red, |
| 197 | | 'WARNING': colors.yellow, |
| 198 | | 'INFO': colors.white |
| 199 | | } |
| 200 | 0 | if(message){ |
| 201 | 0 | console.log(colors.dkgray +'[' + dateString + ']: ' + colors.reset, mapColors[type] + type + colors.reset, '\t', message); |
| 202 | | } |
| 203 | | else{ |
| 204 | 0 | console.log(colors.dkgray +'[' + dateString + ']: ' + colors.reset, mapColors[type] + type + colors.reset); |
| 205 | | } |
| 206 | | } |
| 207 | 738 | return self; |
| 208 | | }, |
| 209 | | |
| 210 | | /** |
| 211 | | * 检查返回值是否有错误,适用于所有WebDriver API的返回值 |
| 212 | | * @method isError |
| 213 | | * @public |
| 214 | | * @param {Object} result 所有WebDriver API的返回值 |
| 215 | | * @return {Boolean} 如果有错误返回true |
| 216 | | */ |
| 217 | | isError: function(result){ |
| 218 | 196 | var status = result && result.status; |
| 219 | 196 | return status !== undefined && status !== 0; |
| 220 | | }, |
| 221 | | |
| 222 | | /** |
| 223 | | * 检查返回值是否正确,适用于所有WebDriver API的返回值 |
| 224 | | * @method isOk |
| 225 | | * @public |
| 226 | | * @param {Object} result 所有WebDriver API的返回值 |
| 227 | | * @return {Boolean} 如果正确返回true |
| 228 | | */ |
| 229 | | isOk: function(result){ |
| 230 | 104 | return !this.isError(result); |
| 231 | | }, |
| 232 | | |
| 233 | | /** |
| 234 | | * 执行webdriver命令 |
| 235 | | * @method doCommand |
| 236 | | * @public |
| 237 | | * @param {String} cmd 命令名称 |
| 238 | | * @param {Object} data 命令数据 |
| 239 | | * @param {Function} [callback] 回调函数,如果省略此参数,则为同步模式,直接返回结果 |
| 240 | | * @return {Object} webdriver返回的JSON对象 |
| 241 | | */ |
| 242 | | doCommand: function(cmd, data, callback){ |
| 243 | 334 | var self = this, cmdInfo = arrCommands[cmd]; |
| 244 | 334 | if(cmdInfo){ |
| 245 | 334 | var method = cmdInfo[0], path = cmdInfo[1]; |
| 246 | 334 | path = path.replace(':sessionId', self.sessionId); |
| 247 | 334 | var pathValues; |
| 248 | 334 | if(data && (pathValues = data.pathValues)){ |
| 249 | | //填充PATH中除sessionId以外的参数 |
| 250 | 118 | for(var name in pathValues){ |
| 251 | 132 | path = path.replace(':'+name, encodeURIComponent(pathValues[name])); |
| 252 | | } |
| 253 | 118 | delete data.pathValues; |
| 254 | | } |
| 255 | 334 | var requestOptions = { |
| 256 | | 'method': method, |
| 257 | | 'host': self.host, |
| 258 | | 'port': self.port, |
| 259 | | 'path': '/wd/hub'+path |
| 260 | | }; |
| 261 | | |
| 262 | 334 | function getResult(result){ |
| 263 | 334 | if(result){ |
| 264 | 220 | result = result.replace(/\x00/g,'') |
| 265 | 220 | try{ |
| 266 | 220 | result = JSON.parse(result); |
| 267 | | } |
| 268 | | catch(err){ |
| 269 | 0 | if (result !== '') { |
| 270 | 0 | self.log('ERROR', err + '\n' + result + '\n') |
| 271 | | } |
| 272 | 0 | result = {status: -1, errorType: 'jsonError', errorMessage: 'JSON.parse error.'}; |
| 273 | 0 | return callback?callback(result):result; |
| 274 | | } |
| 275 | | |
| 276 | 220 | if (result.status === 0) {//操作成功 |
| 277 | 206 | self.log('RESULT', result.value); |
| 278 | 206 | result = result.value; |
| 279 | | } |
| 280 | | else { |
| 281 | 14 | var responseInfo = responseCodes[result.status], |
| 282 | | responseType = responseInfo.type, |
| 283 | | responseMessage = responseInfo.message; |
| 284 | | |
| 285 | 14 | result.errorType = responseType; |
| 286 | 14 | result.errorMessage = responseMessage; |
| 287 | | |
| 288 | 14 | self.log('ERROR', responseType); |
| 289 | | } |
| 290 | | } |
| 291 | 334 | if(callback){ |
| 292 | 14 | callback(result); |
| 293 | | } |
| 294 | | else{ |
| 295 | 320 | return result; |
| 296 | | } |
| 297 | | } |
| 298 | 334 | if(callback){ |
| 299 | 14 | self._doRequest(requestOptions, data, getResult); |
| 300 | | } |
| 301 | | else{ |
| 302 | 320 | var response = self._doRequest(requestOptions, data); |
| 303 | 320 | return getResult(response); |
| 304 | | } |
| 305 | | } |
| 306 | | }, |
| 307 | | |
| 308 | | /** |
| 309 | | * 发起HTTP请求到webdriver的接口 |
| 310 | | * @method _doRequest |
| 311 | | * @private |
| 312 | | * @param {Object} requestOptions HTTP请求参数 |
| 313 | | * @param {Object} data HTTP BODY |
| 314 | | * @param {Function} [callback] 回调函数,活力回调函数则为同步返回模式 |
| 315 | | * @return {String} HTTP响应字符串 |
| 316 | | */ |
| 317 | | _doRequest: function(requestOptions, data, callback){ |
| 318 | 334 | var self = this, fiber = Fiber.current; |
| 319 | | |
| 320 | 334 | self.log('COMMAND', requestOptions.method + '\t' + requestOptions.path); |
| 321 | | |
| 322 | 334 | var responseStr = ''; |
| 323 | | |
| 324 | 334 | if(data){ |
| 325 | 276 | data = JSON.stringify(data); |
| 326 | | //转义Unicode字符 |
| 327 | 276 | data = data.replace(/[^\x00-\xff]/g, function(a){ |
| 328 | 4 | return '\\'+escape(a).substr(1); |
| 329 | | }); |
| 330 | 276 | if(data !== '{}'){ |
| 331 | 178 | self.log('DATA', data); |
| 332 | | } |
| 333 | | } |
| 334 | | else{ |
| 335 | 58 | data = ''; |
| 336 | | } |
| 337 | 334 | requestOptions.headers = { |
| 338 | | 'Accept': 'application/json; charset=utf-8', |
| 339 | | 'Content-Type': 'application/json;charset=UTF-8', |
| 340 | | 'Content-Length': data.length |
| 341 | | } |
| 342 | | |
| 343 | 334 | var req = http.request(requestOptions, function(res){ |
| 344 | 334 | if ( /^302|303$/.test(res.statusCode) && self.sessionId === null) { |
| 345 | 4 | try{ |
| 346 | 4 | var match = res.headers.location.match(/wd\/hub\/session\/(.+)$/i); |
| 347 | 4 | if(match !== null){ |
| 348 | 4 | self.sessionId = match[1]; |
| 349 | 4 | self.log('RESULT', self.sessionId); |
| 350 | | } |
| 351 | | } |
| 352 | | catch(e){ |
| 353 | | } |
| 354 | 4 | return doCallback(''); |
| 355 | | } |
| 356 | 330 | var arrResBuffers = [], resBufferSize = 0; |
| 357 | 330 | res.on('data', function (data) { |
| 358 | 1014 | arrResBuffers.push(data); |
| 359 | 1014 | resBufferSize += data.length; |
| 360 | | }); |
| 361 | 330 | res.on('end', function () { |
| 362 | 330 | var resBuffer = new Buffer(resBufferSize), pos = 0; |
| 363 | 330 | for(var i = 0, c = arrResBuffers.length; i < c; i++) { |
| 364 | 1014 | arrResBuffers[i].copy(resBuffer, pos); |
| 365 | 1014 | pos += arrResBuffers[i].length; |
| 366 | | } |
| 367 | 330 | responseStr = resBuffer.toString('utf-8'); |
| 368 | 330 | doCallback(responseStr); |
| 369 | | }); |
| 370 | | }); |
| 371 | | |
| 372 | 334 | req.on('error', function(err) |
| 373 | | { |
| 374 | 0 | self.log('ERROR', 'ERROR ON REQUEST'); |
| 375 | | }); |
| 376 | | |
| 377 | 334 | if(data){ |
| 378 | 276 | req.write(data); |
| 379 | | } |
| 380 | 334 | req.end(); |
| 381 | | |
| 382 | 334 | function doCallback(data){ |
| 383 | 334 | if(callback){//异步模式 |
| 384 | 14 | callback(data); |
| 385 | | } |
| 386 | 320 | else if(fiber){//同步模式 |
| 387 | 320 | fiber.run(); |
| 388 | | } |
| 389 | | } |
| 390 | | |
| 391 | 334 | if(callback === undefined && fiber){ |
| 392 | 320 | Fiber.yield(); |
| 393 | 320 | return responseStr; |
| 394 | | } |
| 395 | | }, |
| 396 | | |
| 397 | | /** |
| 398 | | * 延迟一定时间 |
| 399 | | * @method sleep |
| 400 | | * @public |
| 401 | | * @param {Number} ms 需要延迟的时间,单位毫秒 |
| 402 | | * @return {Browser} 当前Browser对象实例 |
| 403 | | */ |
| 404 | | sleep: function(ms){ |
| 405 | 20 | var fiber = Fiber.current; |
| 406 | 20 | setTimeout(function(){ |
| 407 | 20 | fiber.run(); |
| 408 | | },ms); |
| 409 | 20 | Fiber.yield(); |
| 410 | 20 | return this; |
| 411 | | }, |
| 412 | | |
| 413 | | /** |
| 414 | | * 返回窗口句柄 |
| 415 | | * @method window |
| 416 | | * @public |
| 417 | | * @param {Boolean} [bAll] 是否返回所有窗口,设置为true返回所有窗口 |
| 418 | | * @return {String|Array} 单个或数组形式的窗口句柄 |
| 419 | | */ |
| 420 | | window: function(bAll){ |
| 421 | 6 | return this.doCommand(bAll === true?'getWindows':'getWindow'); |
| 422 | | }, |
| 423 | | |
| 424 | | /** |
| 425 | | * 切换到另一个Window或Frame |
| 426 | | * @method switchTo |
| 427 | | * @public |
| 428 | | * @param {String|Element|Number} [target] 要切换的目标窗口句柄、Frame对象(Element)、Frame在页面中的序号(Number),如果省略此参数则切换到主窗口 |
| 429 | | * @return {Browser} 当前Browser对象实例 |
| 430 | | */ |
| 431 | | switchTo: function(target){ |
| 432 | 8 | var self = this; |
| 433 | 8 | if(typeof target === 'string'){ |
| 434 | 4 | self.windowHandle = target; |
| 435 | 4 | self.doCommand('switchWindow',{name: target}); |
| 436 | | } |
| 437 | 4 | else if(target instanceof Element){ |
| 438 | 2 | self.doCommand('switchFrame',{id: target.toArray()}); |
| 439 | | } |
| 440 | 2 | else if(target === undefined){ |
| 441 | 2 | self.doCommand('switchFrame',{id: null}); |
| 442 | | } |
| 443 | 8 | return self; |
| 444 | | }, |
| 445 | | |
| 446 | | /** |
| 447 | | * 返回或者设置当前窗口的大小 |
| 448 | | * @method size |
| 449 | | * @public |
| 450 | | * @param {Number|Object} [width] 宽度或者大小对象({width,height}) |
| 451 | | * @param {Number} [height] 高度 |
| 452 | | * @return {Object|Browser} 如果无宽度和高度参数,则返回当前大小对象,否则返回当前Browser实例 |
| 453 | | */ |
| 454 | | size: function(width, height){ |
| 455 | 14 | var self = this, |
| 456 | | windowHandle = self.windowHandle; |
| 457 | 14 | if(width === undefined){ |
| 458 | | //返回首个窗口的大小 |
| 459 | 8 | return self.doCommand('getWindowSize', {'pathValues': {'windowHandle': windowHandle}}); |
| 460 | | } |
| 461 | 6 | else if(width.width !== undefined){ |
| 462 | 2 | height = width.height; |
| 463 | 2 | width = width.width; |
| 464 | | } |
| 465 | 6 | self.doCommand('setWindowSize', { |
| 466 | | 'pathValues': {'windowHandle': windowHandle}, |
| 467 | | 'width': width, |
| 468 | | 'height': height |
| 469 | | }); |
| 470 | 6 | return self; |
| 471 | | }, |
| 472 | | |
| 473 | | /** |
| 474 | | * 最大化当前窗口 |
| 475 | | * @method maximize |
| 476 | | * @public |
| 477 | | * @return {Browser} 当前Browser对象实例 |
| 478 | | */ |
| 479 | | maximize: function(){ |
| 480 | 2 | var self = this; |
| 481 | 2 | self.doCommand('maximizeWindow', {'pathValues': {'windowHandle': self.windowHandle}}); |
| 482 | 2 | return self; |
| 483 | | }, |
| 484 | | |
| 485 | | /** |
| 486 | | * 返回或者设置当前窗口的坐标 |
| 487 | | * @method offset |
| 488 | | * @public |
| 489 | | * @param {Number|Object} [x] x坐标或者坐标对象({x,y}) |
| 490 | | * @param {Number} [y] y坐标 |
| 491 | | * @return {Object|Browser} 如果无x坐标和y坐标参数,则返回当前坐标对象,否则返回当前Browser实例 |
| 492 | | */ |
| 493 | | offset: function(x, y){ |
| 494 | 8 | var self = this, |
| 495 | | windowHandle = self.windowHandle; |
| 496 | 8 | if(x === undefined){ |
| 497 | | //返回首个窗口的大小 |
| 498 | 4 | return self.doCommand('getWindowOffset', {'pathValues': {'windowHandle': windowHandle}}); |
| 499 | | } |
| 500 | 4 | else if(x.x !== undefined){ |
| 501 | 2 | y = x.y; |
| 502 | 2 | x = x.x; |
| 503 | | } |
| 504 | 4 | self.doCommand('setWindowOffset', { |
| 505 | | 'pathValues': {'windowHandle': windowHandle}, |
| 506 | | 'x': x, |
| 507 | | 'y': y |
| 508 | | }); |
| 509 | 4 | return self; |
| 510 | | }, |
| 511 | | |
| 512 | | /** |
| 513 | | * 关闭当前窗口 |
| 514 | | * @method close |
| 515 | | * @public |
| 516 | | * @return {Browser} 当前Browser对象实例 |
| 517 | | */ |
| 518 | | close: function(){ |
| 519 | 6 | var self = this; |
| 520 | 6 | self.doCommand('closeWindow'); |
| 521 | 6 | return self; |
| 522 | | }, |
| 523 | | |
| 524 | | /** |
| 525 | | * 结束浏览器会话 |
| 526 | | * @method end |
| 527 | | * @public |
| 528 | | * @return {Browser} 当前Browser对象实例 |
| 529 | | */ |
| 530 | | end: function(){ |
| 531 | 0 | var self = this; |
| 532 | 0 | self.doCommand('delSession'); |
| 533 | 0 | return self; |
| 534 | | }, |
| 535 | | |
| 536 | | /** |
| 537 | | * 设置操作超时时间 |
| 538 | | * <p> |
| 539 | | * 注:script包括同步和异步两种代码 |
| 540 | | * </p> |
| 541 | | * @method setTimeout |
| 542 | | * @public |
| 543 | | * @param {String} type 操作类型(script|ascript|implicit|page load) |
| 544 | | * @param {Number} ms 超时时间 |
| 545 | | * @return {Browser} 当前Browser对象实例 |
| 546 | | */ |
| 547 | | setTimeout: function(type, ms){ |
| 548 | 4 | var self = this; |
| 549 | 4 | if(type === 'ascript'){ |
| 550 | 4 | self.doCommand('setAscriptTimeout', {'ms': ms}); |
| 551 | | } |
| 552 | | else{ |
| 553 | 0 | self.doCommand('setTimeouts', {'type': type, 'ms': ms}); |
| 554 | | } |
| 555 | 4 | return self; |
| 556 | | }, |
| 557 | | |
| 558 | | /** |
| 559 | | * 打开或者返回当前URL |
| 560 | | * @method url |
| 561 | | * @public |
| 562 | | * @param {String} [url] 需要打开的URL网址,若省略此参数则返回当前浏览器URL |
| 563 | | * @return {String|Browser} 若打开URL则返回当前Browser对象实例,否则返回URL地址 |
| 564 | | */ |
| 565 | | url: function(url){ |
| 566 | 24 | var self = this; |
| 567 | 24 | if(url){ |
| 568 | 14 | self.doCommand('setUrl', {'url': url}); |
| 569 | 14 | return self; |
| 570 | | } |
| 571 | | else { |
| 572 | 10 | return self.doCommand('getUrl'); |
| 573 | | } |
| 574 | | }, |
| 575 | | |
| 576 | | /** |
| 577 | | * 控制浏览器回到后一个URL |
| 578 | | * @method forward |
| 579 | | * @public |
| 580 | | * @return {Browser} 当前Browser对象实例 |
| 581 | | */ |
| 582 | | forward: function(){ |
| 583 | 2 | var self = this; |
| 584 | 2 | self.doCommand('setForward'); |
| 585 | 2 | return self; |
| 586 | | }, |
| 587 | | |
| 588 | | /** |
| 589 | | * 控制浏览器回到前一个URL |
| 590 | | * @method back |
| 591 | | * @public |
| 592 | | * @return {Browser} 当前Browser对象实例 |
| 593 | | */ |
| 594 | | back: function(){ |
| 595 | 2 | var self = this; |
| 596 | 2 | self.doCommand('setBack'); |
| 597 | 2 | return self; |
| 598 | | }, |
| 599 | | |
| 600 | | /** |
| 601 | | * 刷新当前页面 |
| 602 | | * @method refresh |
| 603 | | * @public |
| 604 | | * @return {Browser} 当前Browser对象实例 |
| 605 | | */ |
| 606 | | refresh: function(){ |
| 607 | 2 | var self = this; |
| 608 | 2 | self.doCommand('setRefresh'); |
| 609 | 2 | return self; |
| 610 | | }, |
| 611 | | |
| 612 | | /** |
| 613 | | * 返回当前页面title |
| 614 | | * @method title |
| 615 | | * @public |
| 616 | | * @return {String} 页面title |
| 617 | | */ |
| 618 | | title: function(){ |
| 619 | 2 | return this.doCommand('getTitle'); |
| 620 | | }, |
| 621 | | |
| 622 | | /** |
| 623 | | * 返回当前页面源代码 |
| 624 | | * @method source |
| 625 | | * @public |
| 626 | | * @return {String} 页面源代码 |
| 627 | | */ |
| 628 | | source: function(){ |
| 629 | 2 | return this.doCommand('getSource'); |
| 630 | | }, |
| 631 | | |
| 632 | | /** |
| 633 | | * 返回当前页面下所有cookie |
| 634 | | * @method getCookies |
| 635 | | * @public |
| 636 | | * @return {Array} cookie对象数组 |
| 637 | | */ |
| 638 | | getCookies: function(){ |
| 639 | 10 | return this.doCommand('getAllCookie'); |
| 640 | | }, |
| 641 | | |
| 642 | | /** |
| 643 | | * 添加cookie到当前页面 |
| 644 | | * <p> |
| 645 | | * 这里可以查看WebDriver官方定义的<a href="http://code.google.com/p/selenium/wiki/JsonWireProtocol#Cookie_JSON_Object" target="_blank">cookie对象</a> |
| 646 | | * </p> |
| 647 | | * @method addCookie |
| 648 | | * @public |
| 649 | | * @param {Object} cookie cookie对象 |
| 650 | | * @return {Browser} 当前Browser对象实例 |
| 651 | | */ |
| 652 | | addCookie: function(cookie){ |
| 653 | 4 | var self = this; |
| 654 | 4 | self.doCommand('setCookie', {'cookie': cookie}); |
| 655 | 4 | return self; |
| 656 | | }, |
| 657 | | |
| 658 | | /** |
| 659 | | * 删除cookie |
| 660 | | * @method delCookies |
| 661 | | * @public |
| 662 | | * @param {String} name cookie name |
| 663 | | * @return {Browser} 当前Browser对象实例 |
| 664 | | */ |
| 665 | | delCookies: function(name){ |
| 666 | 4 | var self = this; |
| 667 | 4 | if(name !== undefined){ |
| 668 | 2 | self.doCommand('delCookie', {'pathValues': {'name': name}}); |
| 669 | | } |
| 670 | | else{ |
| 671 | 2 | self.doCommand('delAllCookies'); |
| 672 | | } |
| 673 | 4 | return self; |
| 674 | | }, |
| 675 | | |
| 676 | | /** |
| 677 | | * 执行Javascript脚本 |
| 678 | | * @method exec |
| 679 | | * @public |
| 680 | | * @param {String} script Javascript代码 |
| 681 | | * @param {Array|Object} [args] 传递给脚本的参数,此参数可省略 |
| 682 | | * @param {Boolean} bAsync 是否异步,如果是异步必需要调用callback返回,否则一直阻塞 |
| 683 | | * @return {Object} Javascript脚本的返回值 |
| 684 | | */ |
| 685 | | exec: function(script, args, bAsync){ |
| 686 | 24 | if(typeof args === 'boolean'){ |
| 687 | 4 | bAsync = args; |
| 688 | 4 | args = []; |
| 689 | | } |
| 690 | 24 | args = args?args:[]; |
| 691 | 24 | if(Object.prototype.toString.apply(args) !== '[object Array]'){ |
| 692 | | //单参数模式 |
| 693 | 6 | args = [args]; |
| 694 | | } |
| 695 | 24 | var arg; |
| 696 | 24 | for(var i in args){ |
| 697 | 14 | arg = args[i]; |
| 698 | 14 | if(arg instanceof Element){ |
| 699 | | //转换为原生Element对象 |
| 700 | 2 | args[i] = arg.toArray(); |
| 701 | | } |
| 702 | | } |
| 703 | 24 | var data = {'script': script, args: args}; |
| 704 | 24 | return this.doCommand(bAsync === true?'executeAsync':'execute', data); |
| 705 | | }, |
| 706 | | |
| 707 | | /** |
| 708 | | * 获取弹出窗口的文本(alert, confirm, prompt) |
| 709 | | * @method getAlert |
| 710 | | * @public |
| 711 | | * @return {String} 弹出窗口的文本 |
| 712 | | */ |
| 713 | | getAlert: function(){ |
| 714 | 2 | return this.doCommand('getAlert'); |
| 715 | | }, |
| 716 | | |
| 717 | | /** |
| 718 | | * 输入prompt中的值 |
| 719 | | * @method setAlert |
| 720 | | * @public |
| 721 | | * @return {Browser} 当前Browser对象实例 |
| 722 | | */ |
| 723 | | setAlert: function(str){ |
| 724 | 0 | var self = this; |
| 725 | 0 | self.doCommand('setAlert', {'text': str}); |
| 726 | 0 | return self; |
| 727 | | }, |
| 728 | | |
| 729 | | /** |
| 730 | | * 关闭弹出窗口 |
| 731 | | * @method closeAlert |
| 732 | | * @public |
| 733 | | * @param {String} [btn] 点击哪个按钮关闭窗口:ok|cancel,默认cancel |
| 734 | | * @return {Browser} 当前Browser对象实例 |
| 735 | | */ |
| 736 | | closeAlert: function(btn){ |
| 737 | 2 | var self = this; |
| 738 | 2 | self.doCommand(/^ok$/i.test(btn)?'acceptAlert':'dismissAlert'); |
| 739 | 2 | return self; |
| 740 | | }, |
| 741 | | |
| 742 | | /** |
| 743 | | * 获取或保存当前网页截图 |
| 744 | | * <p> |
| 745 | | * filePath为可选项,若有则保存到文件 |
| 746 | | * </p> |
| 747 | | * @method getScreenshot |
| 748 | | * @public |
| 749 | | * @param {String} [filePath] 保存截图的文件路径 |
| 750 | | * @return {String} Base64格式的网页截图 |
| 751 | | */ |
| 752 | | getScreenshot: function(filePath){ |
| 753 | 2 | var data = this.doCommand('getScreenshot'); |
| 754 | 2 | if(filePath){ |
| 755 | 2 | fs.writeFileSync(filePath, data, "base64"); |
| 756 | | } |
| 757 | 2 | return data; |
| 758 | | }, |
| 759 | | |
| 760 | | /** |
| 761 | | * 获取按键映射后的字符数组 |
| 762 | | * @method _getKeyArray |
| 763 | | * @private |
| 764 | | * @param {String} str 原始字符串 |
| 765 | | * @return {Array} 经过映射后的字符数组 |
| 766 | | */ |
| 767 | | _getKeyArray: function(str){ |
| 768 | 14 | str = str.replace(/{(\w+)}/g, function(all, name){ |
| 769 | 4 | var key = arrSpecKeys[name.toLowerCase()]; |
| 770 | 4 | return key?key:all; |
| 771 | | }); |
| 772 | 14 | return str.split(''); |
| 773 | | }, |
| 774 | | |
| 775 | | /** |
| 776 | | * 发送按键序列到当前焦点对象上 |
| 777 | | * @method sendKeys |
| 778 | | * @public |
| 779 | | * @param {String} str 原始字符串 |
| 780 | | * @return {Browser} 当前Browser对象实例 |
| 781 | | */ |
| 782 | | sendKeys: function(str){ |
| 783 | 2 | var self = this; |
| 784 | 2 | self.doCommand('sendKeys', {'value': self._getKeyArray(str)}); |
| 785 | 2 | return self; |
| 786 | | }, |
| 787 | | |
| 788 | | /** |
| 789 | | * 在当前光标处触发单击事件 |
| 790 | | * @method click |
| 791 | | * @public |
| 792 | | * @param {String} mouseBtn 鼠标按钮类型:left|middle|right |
| 793 | | * @return {Browser} 当前Browser对象实例 |
| 794 | | */ |
| 795 | | click: function(mouseBtn){ |
| 796 | 4 | var self = this, data = {}; |
| 797 | 4 | if(mouseBtn){ |
| 798 | 0 | var button = arrMouseButton[mouseBtn.toLowerCase()]; |
| 799 | 0 | if(button){ |
| 800 | 0 | data.button = button; |
| 801 | | } |
| 802 | | } |
| 803 | 4 | self.doCommand('click', data); |
| 804 | 4 | return self; |
| 805 | | }, |
| 806 | | |
| 807 | | /** |
| 808 | | * 在当前光标处触发双击事件 |
| 809 | | * @method dblclick |
| 810 | | * @public |
| 811 | | * @return {Browser} 当前Browser对象实例 |
| 812 | | */ |
| 813 | | dblclick: function(){ |
| 814 | 2 | var self = this; |
| 815 | 2 | self.doCommand('dblclick'); |
| 816 | 2 | return self; |
| 817 | | }, |
| 818 | | |
| 819 | | /** |
| 820 | | * 在当前光标处触发鼠标左键按下 |
| 821 | | * @method mousedown |
| 822 | | * @public |
| 823 | | * @return {Browser} 当前Browser对象实例 |
| 824 | | */ |
| 825 | | mousedown: function(){ |
| 826 | 2 | var self = this; |
| 827 | 2 | self.doCommand('mousedown'); |
| 828 | 2 | return self; |
| 829 | | }, |
| 830 | | |
| 831 | | /** |
| 832 | | * 移动到指定对象中间或坐标点 |
| 833 | | * @method mousemove |
| 834 | | * @public |
| 835 | | * @param {Number|Element|Object} x X坐标或Element对象或{x,y} |
| 836 | | * @param {Number} y y坐标 |
| 837 | | * @return {Browser} 当前Browser对象实例 |
| 838 | | */ |
| 839 | | mousemove: function(x, y){ |
| 840 | 6 | var self = this, data = {}; |
| 841 | 6 | if(x instanceof Element){ |
| 842 | 4 | data.element = x.toArray().ELEMENT; |
| 843 | | } |
| 844 | 2 | else if(x.x && x.y){ |
| 845 | 0 | data.xoffset = x.x; |
| 846 | 0 | data.yoffset = x.y; |
| 847 | | } |
| 848 | | else{ |
| 849 | 2 | data.xoffset = x; |
| 850 | 2 | data.yoffset = y; |
| 851 | | } |
| 852 | 6 | if(data.element === undefined){ |
| 853 | 2 | data.xoffset = Math.round(data.xoffset); |
| 854 | 2 | data.yoffset = Math.round(data.yoffset); |
| 855 | | } |
| 856 | 6 | self.doCommand('mousemove', data); |
| 857 | 6 | return self; |
| 858 | | }, |
| 859 | | |
| 860 | | /** |
| 861 | | * 在当前光标处触发鼠标左键按起 |
| 862 | | * @method mouseup |
| 863 | | * @public |
| 864 | | * @return {Browser} 当前Browser对象实例 |
| 865 | | */ |
| 866 | | mouseup: function(){ |
| 867 | 2 | var self = this; |
| 868 | 2 | self.doCommand('mouseup'); |
| 869 | 2 | return self; |
| 870 | | }, |
| 871 | | |
| 872 | | /** |
| 873 | | * 拖放 |
| 874 | | * @method dragDrop |
| 875 | | * @public |
| 876 | | * @param {Element|Object} from 源对象或源坐标({x,y}) |
| 877 | | * @param {Element|Object} to 目标对象或目标坐标({x,y}) |
| 878 | | * @return {Browser} 当前Browser对象实例 |
| 879 | | */ |
| 880 | | dragDrop: function(from, to){ |
| 881 | 0 | var self = this; |
| 882 | 0 | self.mousemove(from).mousedown().mousemove(to).mouseup(); |
| 883 | 0 | return self; |
| 884 | | }, |
| 885 | | |
| 886 | | /** |
| 887 | | * 等待对象出现或消失 |
| 888 | | * @method waitFor |
| 889 | | * @public |
| 890 | | * @param {String} [using] 对象选择类型,留空默认为:css selector(class name|css selector|id|name|link text|partial link text|tag name|xpath) |
| 891 | | * @param {String} value 对象选择值 |
| 892 | | * @param {Boolean} [targetExist] 测试目标是对象存在或不存在 |
| 893 | | * @param {Number} [timeout] 等待超时时间,单位毫秒,默认30000毫秒 |
| 894 | | * @return {Element|Object} 等待的目标如果存在,则返回Element对象,如果不存在或者超时则返回错误对象 |
| 895 | | */ |
| 896 | | waitFor: function(using, value, targetExist, timeout){ |
| 897 | 6 | var self = this, |
| 898 | | fiber = Fiber.current, |
| 899 | | ret, bExist, bTimeout = false; |
| 900 | 6 | if(typeof value !== 'string'){ |
| 901 | | //默认css选择器 |
| 902 | 6 | timeout = targetExist; |
| 903 | 6 | targetExist = value; |
| 904 | 6 | value = using; |
| 905 | 6 | using = 'css selector'; |
| 906 | | } |
| 907 | 6 | if(typeof targetExist === 'number'){ |
| 908 | 2 | timeout = targetExist; |
| 909 | 2 | targetExist = true; |
| 910 | | } |
| 911 | 6 | if(targetExist === undefined){ |
| 912 | 2 | targetExist = true; |
| 913 | | } |
| 914 | 6 | if(timeout === undefined){ |
| 915 | 4 | timeout = 30000; |
| 916 | | } |
| 917 | 6 | var _timer1, _timer2; |
| 918 | 6 | function waitElement(){ |
| 919 | 10 | self.doCommand('getElement', {'using': using,'value': value}, function(result){ |
| 920 | 10 | bExist = self.isOk(result); |
| 921 | 10 | ret = result; |
| 922 | 10 | if(bExist === targetExist){ |
| 923 | 4 | clearTimeout(_timer2); |
| 924 | 4 | fiber.run(); |
| 925 | | } |
| 926 | 6 | else if(bTimeout === true){ |
| 927 | 2 | fiber.run(); |
| 928 | | } |
| 929 | 4 | else if(_timer1 !== null){ |
| 930 | | //每隔500毫秒确认目标是否存在 |
| 931 | 4 | _timer1 = setTimeout(waitElement, 500); |
| 932 | | } |
| 933 | | }); |
| 934 | | } |
| 935 | 6 | _timer1 = setTimeout(waitElement, 1); |
| 936 | 6 | _timer2 = setTimeout(function(){ |
| 937 | 2 | self.log('ERROR', 'waitFor timeout: ' + using + ' , ' + value); |
| 938 | | //标记超时,getElement返回时结束当前等待请求 |
| 939 | 2 | bTimeout = true; |
| 940 | | }, timeout); |
| 941 | 6 | Fiber.yield(); |
| 942 | 6 | return bExist === true ? self.element('element', ret.ELEMENT) : ret; |
| 943 | | }, |
| 944 | | |
| 945 | | /** |
| 946 | | * 返回Element对象实例 |
| 947 | | * <p> |
| 948 | | * 此方法不建议直接调用,建议使用run函数的第二个参数$符,例如: |
| 949 | | * <pre> |
| 950 | | * wd.run(function(browser, $){ |
| 951 | | * $('#id').val('test'); |
| 952 | | * }) |
| 953 | | * </pre> |
| 954 | | * </p> |
| 955 | | * @method element |
| 956 | | * @public |
| 957 | | * @param {String} [using] 对象选择类型,留空默认为:css selector(class name|css selector|id|name|link text|partial link text|tag name|xpath) |
| 958 | | * @param {String} [value] 对象选择值,留空返回当前焦点所在对象 |
| 959 | | * @return {Element} Element的实例对象,如果对象不存在,返回原始消息 |
| 960 | | */ |
| 961 | | element: function(using, value){ |
| 962 | 82 | var self = this; |
| 963 | 82 | var element = new Element(self, using, value); |
| 964 | 82 | return self.isError(element._id) ? element._id : element; |
| 965 | | } |
| 966 | | |
| 967 | | } |
| 968 | | |
| 969 | 1 | module.exports = Browser; |