1 | | /*! |
2 | | * |
3 | | * Copyright (c) 2013 Sebastian Golasch |
4 | | * |
5 | | * Permission is hereby granted, free of charge, to any person obtaining a |
6 | | * copy of this software and associated documentation files (the "Software"), |
7 | | * to deal in the Software without restriction, including without limitation |
8 | | * the rights to use, copy, modify, merge, publish, distribute, sublicense, |
9 | | * and/or sell copies of the Software, and to permit persons to whom the |
10 | | * Software is furnished to do so, subject to the following conditions: |
11 | | * |
12 | | * The above copyright notice and this permission notice shall be included |
13 | | * in all copies or substantial portions of the Software. |
14 | | * |
15 | | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
16 | | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
17 | | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
18 | | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
19 | | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
20 | | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
21 | | * DEALINGS IN THE SOFTWARE. |
22 | | * |
23 | | */ |
24 | | |
25 | 1 | 'use strict'; |
26 | | |
27 | | // ext. libs |
28 | 1 | var Q = require('q'); |
29 | 1 | var os = require('os'); |
30 | 1 | var cp = require('child_process'); |
31 | 1 | var appium = require('appium/lib/server/main'); |
32 | 1 | var portscanner = require('portscanner'); |
33 | | |
34 | | /** |
35 | | * This module is a browser plugin for [DalekJS](//github.com/dalekjs/dalek). |
36 | | * It provides all a WebDriverServer & browser launcher for Safari on iOS. |
37 | | * |
38 | | * At the moment this only works with the IPhone |
39 | | * |
40 | | * The browser plugin can be installed with the following command: |
41 | | * |
42 | | * ```bash |
43 | | * $ npm install dalek-browser-ios --save-dev |
44 | | * ``` |
45 | | * |
46 | | * You can use the browser plugin by adding a config option to the your Dalekfile |
47 | | * |
48 | | * ```javascript |
49 | | * "browsers": ["ios"] |
50 | | * ``` |
51 | | * |
52 | | * Or you can tell Dalek that it should test in this browser via the command line: |
53 | | * |
54 | | * ```bash |
55 | | * $ dalek mytest.js -b ios |
56 | | * ``` |
57 | | * |
58 | | * The Webdriver Server tries to open Port 9003 by default, |
59 | | * if this port is blocked, it tries to use a port between 9004 & 9093 |
60 | | * You can specifiy a different port from within your [Dalekfile](/pages/config.html) like so: |
61 | | * |
62 | | * ```javascript |
63 | | * "browsers": { |
64 | | * "ios": { |
65 | | * "port": 5555 |
66 | | * } |
67 | | * } |
68 | | * ``` |
69 | | * |
70 | | * It is also possible to specify a range of ports: |
71 | | * |
72 | | * ```javascript |
73 | | * "browsers": { |
74 | | * "ios": { |
75 | | * "portRange": [6100, 6120] |
76 | | * } |
77 | | * } |
78 | | * ``` |
79 | | * |
80 | | * If you would like to test on the IPad (IPhone) emulator, you can simply apply a snd. argument, |
81 | | * which defines the browser type: |
82 | | * |
83 | | * ```bash |
84 | | * $ dalek mytest.js -b ios:ipad |
85 | | * ``` |
86 | | * |
87 | | * @module DalekJS |
88 | | * @class IosDriver |
89 | | * @namespace Browser |
90 | | * @part iOS |
91 | | * @api |
92 | | */ |
93 | | |
94 | 1 | var IosDriver = { |
95 | | |
96 | | /** |
97 | | * Verbose version of the browser name |
98 | | * |
99 | | * @property longName |
100 | | * @type string |
101 | | * @default Mobile Safari iOS |
102 | | */ |
103 | | |
104 | | longName: 'Mobile Safari iOS (iPhone)', |
105 | | |
106 | | /** |
107 | | * Default port of the Appium WebDriverServer |
108 | | * The port may change, cause the port conflict resolution |
109 | | * tool might pick another one, if the default one is blocked |
110 | | * |
111 | | * @property port |
112 | | * @type integer |
113 | | * @default 4723 |
114 | | */ |
115 | | |
116 | | port: 4723, |
117 | | |
118 | | /** |
119 | | * WebHook port |
120 | | * |
121 | | * @property webhookPort |
122 | | * @type integer |
123 | | * @default 9003 |
124 | | */ |
125 | | |
126 | | webhookPort: 9003, |
127 | | |
128 | | /** |
129 | | * Default host of the Appium WebDriverServer |
130 | | * The host may be overridden with |
131 | | * a user configured value |
132 | | * |
133 | | * @property host |
134 | | * @type string |
135 | | * @default localhost |
136 | | */ |
137 | | |
138 | | host: 'localhost', |
139 | | |
140 | | /** |
141 | | * Root path of the appium webdriver server |
142 | | * |
143 | | * @property path |
144 | | * @type string |
145 | | * @default /wd/hub |
146 | | */ |
147 | | |
148 | | path: '/wd/hub', |
149 | | |
150 | | /** |
151 | | * Default desired capabilities that should be |
152 | | * transferred when the browser session gets requested |
153 | | * |
154 | | * @property desiredCapabilities |
155 | | * @type object |
156 | | */ |
157 | | |
158 | | desiredCapabilities: { |
159 | | device: 'iPhone Emulator', |
160 | | name: 'Safari remote via WD', |
161 | | app: 'safari', |
162 | | version: '6.1', |
163 | | browserName: '' |
164 | | }, |
165 | | |
166 | | /** |
167 | | * Driver defaults, what should the driver be able to access. |
168 | | * |
169 | | * @property driverDefaults |
170 | | * @type object |
171 | | */ |
172 | | |
173 | | driverDefaults: { |
174 | | viewport: true, |
175 | | status: { |
176 | | os: { |
177 | | arch: os.arch(), |
178 | | version: os.release(), |
179 | | name: 'Mac OSX' |
180 | | } |
181 | | }, |
182 | | sessionInfo: true |
183 | | }, |
184 | | |
185 | | /** |
186 | | * Special arguments that are needed to invoke |
187 | | * appium. These are the defaults, they need to be modified later on |
188 | | * |
189 | | * @property appiumArgs |
190 | | * @type object |
191 | | */ |
192 | | |
193 | | appiumArgs: { |
194 | | app: null, |
195 | | ipa: null, |
196 | | quiet: true, |
197 | | udid: null, |
198 | | keepArtifacts: false, |
199 | | noSessionOverride: false, |
200 | | fullReset: false, |
201 | | noReset: false, |
202 | | launch: false, |
203 | | log: false, |
204 | | nativeInstrumentsLib: false, |
205 | | safari: false, |
206 | | forceIphone: false, |
207 | | forceIpad: false, |
208 | | orientation: null, |
209 | | useKeystore: false, |
210 | | address: '0.0.0.0', |
211 | | nodeconfig: null, |
212 | | port: null, |
213 | | webhook: null |
214 | | }, |
215 | | |
216 | | /** |
217 | | * Different browser types (iPhone / iPad) |
218 | | * |
219 | | * @property browserTypes |
220 | | * @type object |
221 | | */ |
222 | | |
223 | | browserTypes: { |
224 | | |
225 | | /** |
226 | | * IPad emulator |
227 | | * |
228 | | * @property ipad |
229 | | * @type object |
230 | | */ |
231 | | |
232 | | ipad: { |
233 | | name: 'iPad' |
234 | | } |
235 | | |
236 | | }, |
237 | | |
238 | | /** |
239 | | * Resolves the driver port |
240 | | * |
241 | | * @method getPort |
242 | | * @return {integer} port WebDriver server port |
243 | | */ |
244 | | |
245 | | getPort: function () { |
246 | 0 | return this.port; |
247 | | }, |
248 | | |
249 | | /** |
250 | | * Resolves the maximum range for the driver port |
251 | | * |
252 | | * @method getMaxPort |
253 | | * @return {integer} port Max WebDriver server port range |
254 | | */ |
255 | | |
256 | | getMaxPort: function () { |
257 | 0 | return this.maxPort; |
258 | | }, |
259 | | |
260 | | /** |
261 | | * Resolves the webhook port |
262 | | * |
263 | | * @method getWebhookPort |
264 | | * @return {integer} WebHook server port |
265 | | */ |
266 | | |
267 | | getWebhookPort: function () { |
268 | 0 | return this.webhookPort; |
269 | | }, |
270 | | |
271 | | /** |
272 | | * Resolves the maximum range for the webhook port |
273 | | * |
274 | | * @method getWebhookPort |
275 | | * @return {integer} WebHook Max WebHook port |
276 | | */ |
277 | | |
278 | | getMaxWebhookPort: function () { |
279 | 0 | return this.maxWebhookPort; |
280 | | }, |
281 | | |
282 | | /** |
283 | | * Returns the driver host |
284 | | * |
285 | | * @method getHost |
286 | | * @return {string} host WebDriver server hostname |
287 | | */ |
288 | | |
289 | | getHost: function () { |
290 | 0 | return this.host; |
291 | | }, |
292 | | |
293 | | /** |
294 | | * Launches appium & corresponding emulator or device, |
295 | | * kicks off the portscanner |
296 | | * |
297 | | * @method launch |
298 | | * @param {object} configuration Browser configuration |
299 | | * @param {EventEmitter2} events EventEmitter (Reporter Emitter instance) |
300 | | * @param {Dalek.Internal.Config} config Dalek configuration class |
301 | | * @return {object} promise Browser promise |
302 | | */ |
303 | | |
304 | | launch: function (configuration, events, config) { |
305 | 0 | var deferred = Q.defer(); |
306 | | |
307 | | // store injected configuration/log event handlers |
308 | 0 | this.reporterEvents = events; |
309 | 0 | this.configuration = configuration; |
310 | 0 | this.config = config; |
311 | | |
312 | | // check if the user wants to run the iPad emulator |
313 | 0 | if (configuration && configuration.type === 'ipad') { |
314 | 0 | this.longName = this.longName.replace('iPhone', 'iPad'); |
315 | 0 | this.appiumArgs.forceIpad = true; |
316 | | } |
317 | | |
318 | | // check for a user set port |
319 | 0 | var browsers = this.config.get('browsers'); |
320 | 0 | if (browsers && Array.isArray(browsers)) { |
321 | 0 | browsers.forEach(this._checkUserDefinedPorts.bind(this)); |
322 | | } |
323 | | |
324 | | // check if the current port is in use, if so, scan for free ports |
325 | 0 | portscanner.findAPortNotInUse(this.getPort(), this.getMaxPort(), this.getHost(), this._checkPorts.bind(this, deferred)); |
326 | 0 | return deferred.promise; |
327 | | }, |
328 | | |
329 | | /** |
330 | | * Kills the Appium Server process, |
331 | | * kills simulator processses |
332 | | * with a slight timeout to prevent |
333 | | * appium from throwing errors |
334 | | * |
335 | | * @method kill |
336 | | * @chainable |
337 | | */ |
338 | | |
339 | | kill: function () { |
340 | | // kill appium servers |
341 | 0 | this.appiumServer.webSocket.server.close(); |
342 | 0 | this.appiumServer.rest.listen().close(); |
343 | | // slight timeout for process killing |
344 | 0 | setTimeout(this._processes.bind(this, this._kill.bind(this)), 1000); |
345 | 0 | return this; |
346 | | }, |
347 | | |
348 | | /** |
349 | | * Kills the non blacklisted simulator processes & restores |
350 | | * the stderr handler |
351 | | * |
352 | | * @method _kill |
353 | | * @param {object|null} err Error or null |
354 | | * @param {array} result List of currently running simulator processes |
355 | | * @chainable |
356 | | * @private |
357 | | */ |
358 | | |
359 | | _kill: function (err, result) { |
360 | | // kill simulator processes |
361 | 0 | result.forEach(this._killProcess.bind(this)); |
362 | | // (re)establish stderr/stdout stream |
363 | 0 | this._reinstantiateLog(); |
364 | 0 | return this; |
365 | | }, |
366 | | |
367 | | /** |
368 | | * Checks a blacklist & kills the process when |
369 | | * not found |
370 | | * |
371 | | * @method _killProcess |
372 | | * @param {integer} processID Process ID |
373 | | * @chainable |
374 | | * @private |
375 | | */ |
376 | | |
377 | | _killProcess: function (processID) { |
378 | 0 | var kill = true; |
379 | | |
380 | | // walk through the list of processes that are |
381 | | // open before the driver started |
382 | 0 | this.openProcesses.forEach(function (pid) { |
383 | 0 | if (pid === processID) { |
384 | 0 | kill = false; |
385 | | } |
386 | | }); |
387 | | |
388 | 0 | if (kill === true) { |
389 | 0 | cp.spawn('kill', [processID]); |
390 | | } |
391 | | |
392 | 0 | return this; |
393 | | }, |
394 | | |
395 | | /** |
396 | | * Checks & switches the appium server port, |
397 | | * scans the range for the webhook port |
398 | | * |
399 | | * @method _listProcesses |
400 | | * @param {object} deferred Promise |
401 | | * @param {object|null} err Error or null |
402 | | * @param {integer} port Appium server port to use |
403 | | * @chainable |
404 | | * @private |
405 | | */ |
406 | | |
407 | | _checkPorts: function (deferred, error, port) { |
408 | | // check if the port was blocked & if we need to switch to another port |
409 | 0 | if (this.port !== port) { |
410 | 0 | this.reporterEvents.emit('report:log:system', 'dalek-browser-ios: Switching to port: ' + port); |
411 | 0 | this.port = port; |
412 | | } |
413 | | |
414 | | // check if the current webhook port is in use, if so, scan for free ports |
415 | 0 | portscanner.findAPortNotInUse(this.getWebhookPort(), this.getMaxWebhookPort(), this.getHost(), this._launch.bind(this, deferred)); |
416 | 0 | return this; |
417 | | }, |
418 | | |
419 | | /** |
420 | | * Checks & switches the webhook port, |
421 | | * loads a list of running simulator processes |
422 | | * |
423 | | * @method _listProcesses |
424 | | * @param {object} deferred Promise |
425 | | * @param {object|null} err Error or null |
426 | | * @param {integer} port Webhook port to use |
427 | | * @chainable |
428 | | * @private |
429 | | */ |
430 | | |
431 | | _launch: function (deferred, error, port) { |
432 | | // check if the port was blocked & if we need to switch to another port |
433 | 0 | if (this.webhookPort !== port) { |
434 | 0 | this.reporterEvents.emit('report:log:system', 'dalek-browser-ios: Switching to webhook port: ' + port); |
435 | 0 | this.webhookPort = port; |
436 | | } |
437 | | |
438 | | // launch appium & the emulator |
439 | 0 | this._processes(this._listProcesses.bind(this, deferred)); |
440 | 0 | return this; |
441 | | }, |
442 | | |
443 | | /** |
444 | | * Stores open processes, |
445 | | * suppresses stdout logs, |
446 | | * starts appium |
447 | | * |
448 | | * @method _listProcesses |
449 | | * @param {object} deferred Promise |
450 | | * @param {object|null} err Error or null |
451 | | * @param {array} result List of currently running simulator processes |
452 | | * @chainable |
453 | | * @private |
454 | | */ |
455 | | |
456 | | _listProcesses: function (deferred, err, result) { |
457 | | // save list of open emulator processes, before we launched it |
458 | 0 | this.openProcesses = result; |
459 | | // nasty hack to surpress socket.io debug reports from appium |
460 | 0 | this._suppressAppiumLogs(); |
461 | | // run appium |
462 | 0 | appium.run(this._loadAppiumArgs(this.appiumArgs), this._afterAppiumStarted.bind(this, deferred)); |
463 | 0 | return this; |
464 | | }, |
465 | | |
466 | | /** |
467 | | * Stores the appium server reference, |
468 | | * restores the stdout logs |
469 | | * |
470 | | * @method _afterAppiumStarted |
471 | | * @param {object} deferred Promise |
472 | | * @param {object} appiumServer Appium server instance |
473 | | * @chainable |
474 | | * @private |
475 | | */ |
476 | | |
477 | | _afterAppiumStarted: function (deferred, appiumServer) { |
478 | 0 | this.appiumServer = appiumServer; |
479 | 0 | deferred.resolve(); |
480 | 0 | return this; |
481 | | }, |
482 | | |
483 | | /** |
484 | | * Configures appium |
485 | | * |
486 | | * @method _loadAppiumArgs |
487 | | * @param {object} appiumArgs Appium specific configuration |
488 | | * @return {object} Modified appium configuration |
489 | | * @private |
490 | | */ |
491 | | |
492 | | _loadAppiumArgs: function (appiumArgs) { |
493 | 0 | appiumArgs.port = this.getPort(); |
494 | 0 | appiumArgs.webhook = this.getHost() + ':' + this.getWebhookPort(); |
495 | 0 | return appiumArgs; |
496 | | }, |
497 | | |
498 | | /** |
499 | | * Process user defined ports |
500 | | * |
501 | | * @method _checkUserDefinedPorts |
502 | | * @param {object} browser Browser configuration |
503 | | * @chainable |
504 | | * @private |
505 | | */ |
506 | | |
507 | | _checkUserDefinedPorts: function (browser) { |
508 | 0 | this._checkAppiumPorts(browser); |
509 | 0 | this._checkWebhookPorts(browser); |
510 | 0 | return this; |
511 | | }, |
512 | | |
513 | | /** |
514 | | * Process user defined appium ports |
515 | | * |
516 | | * @method _checkAppiumPorts |
517 | | * @param {object} browser Browser configuration |
518 | | * @chainable |
519 | | * @private |
520 | | */ |
521 | | |
522 | | _checkAppiumPorts: function (browser) { |
523 | | // check for a single defined port |
524 | 0 | if (browser.ios && browser.ios.port) { |
525 | 0 | this.port = parseInt(browser.ios.port, 10); |
526 | 0 | this.maxPort = this.port + 90; |
527 | 0 | this.reporterEvents.emit('report:log:system', 'dalek-browser-ios: Switching to user defined port: ' + this.port); |
528 | | } |
529 | | |
530 | | // check for a port range |
531 | 0 | if (browser.ios && browser.ios.portRange && browser.ios.portRange.length === 2) { |
532 | 0 | this.port = parseInt(browser.ios.portRange[0], 10); |
533 | 0 | this.maxPort = parseInt(browser.ios.portRange[1], 10); |
534 | 0 | this.reporterEvents.emit('report:log:system', 'dalek-browser-ios: Switching to user defined port(s): ' + this.port + ' -> ' + this.maxPort); |
535 | | } |
536 | | |
537 | 0 | return this; |
538 | | }, |
539 | | |
540 | | /** |
541 | | * Process user defined webhook ports |
542 | | * |
543 | | * @method _checkWebhookPorts |
544 | | * @param {object} browser Browser configuration |
545 | | * @chainable |
546 | | * @private |
547 | | */ |
548 | | |
549 | | _checkWebhookPorts: function (browser) { |
550 | | // check for a single defined webhook port |
551 | 0 | if (browser.ios && browser.ios.webhookPort) { |
552 | 0 | this.webhookPort = parseInt(browser.ios.webhookPort, 10); |
553 | 0 | this.maxWebhookPort = this.webhookPort + 90; |
554 | 0 | this.reporterEvents.emit('report:log:system', 'dalek-browser-ios: Switching to user defined webhook port: ' + this.webhookPort); |
555 | | } |
556 | | |
557 | | // check for a webhook port range |
558 | 0 | if (browser.ios && browser.ios.webhookPortRange && browser.ios.webhookPortRange.length === 2) { |
559 | 0 | this.webhookPort = parseInt(browser.ios.webhookPortRange[0], 10); |
560 | 0 | this.maxWebhookPort = parseInt(browser.ios.webhookPortRange[1], 10); |
561 | 0 | this.reporterEvents.emit('report:log:system', 'dalek-browser-ios: Switching to user defined webhook port(s): ' + this.webhookPort + ' -> ' + this.maxWebhookPort); |
562 | | } |
563 | | |
564 | 0 | return this; |
565 | | }, |
566 | | |
567 | | /** |
568 | | * Tracks running simulator processes |
569 | | * |
570 | | * @method _processes |
571 | | * @param {function} fn Callback |
572 | | * @chainable |
573 | | * @private |
574 | | */ |
575 | | |
576 | | _processes: function (fn) { |
577 | 0 | var cmd = ['ps -ax', '|', 'grep "iPhone Simulator.app"']; |
578 | 0 | cp.exec(cmd.join(' '), this._transformProcesses.bind(this, fn)); |
579 | 0 | return this; |
580 | | }, |
581 | | |
582 | | /** |
583 | | * Transforms the process list output into |
584 | | * a json structure |
585 | | * |
586 | | * @method _transformProcesses |
587 | | * @param {function} fn Callback |
588 | | * @param {null|object} err Error if error, null if not |
589 | | * @param {string} stdout Terminal output |
590 | | * @chainable |
591 | | * @private |
592 | | */ |
593 | | |
594 | | _transformProcesses: function(fn, err, stdout){ |
595 | 0 | var result = []; |
596 | 0 | stdout.split('\n').forEach(this._scanProcess.bind(this, result)); |
597 | 0 | fn(err, result); |
598 | 0 | return this; |
599 | | }, |
600 | | |
601 | | /** |
602 | | * Scans and transforms the process list |
603 | | * |
604 | | * @method _scanProcess |
605 | | * @param {array} result Transformed result |
606 | | * @param {string} line Process list entry |
607 | | * @chainable |
608 | | * @private |
609 | | */ |
610 | | |
611 | | _scanProcess: function (result, line){ |
612 | 0 | var data = line.split(' '); |
613 | 0 | data = data.filter(this._filterProcessItem); |
614 | | |
615 | 0 | if (data[1] === '??') { |
616 | 0 | result.push(data[0]); |
617 | | } |
618 | | |
619 | 0 | return this; |
620 | | }, |
621 | | |
622 | | /** |
623 | | * Filters process list items |
624 | | * |
625 | | * @method _filterProcessItem |
626 | | * @param {string} item Process list entry |
627 | | * @return {bool|string} Process item or false |
628 | | * @private |
629 | | */ |
630 | | |
631 | | _filterProcessItem: function (item) { |
632 | 0 | if (item !== '') { |
633 | 0 | return item; |
634 | | } |
635 | | |
636 | 0 | return false; |
637 | | }, |
638 | | |
639 | | /** |
640 | | * Overwrite default stdout & stderr handler |
641 | | * to suppress some appium logs |
642 | | * |
643 | | * @method _suppressAppiumLogs |
644 | | * @chainable |
645 | | * @private |
646 | | */ |
647 | | |
648 | | _suppressAppiumLogs: function () { |
649 | | // TODO: Check if the log level of appium can be set to 0 |
650 | 0 | var _supLogs = function (data) { |
651 | 0 | if (data.search('6minfo') === -1 && data.search('33mwarn') === -1 && data.search('90mdebug') === -1) { |
652 | 0 | this.oldWrite.bind(process.stdout)(data); |
653 | | } |
654 | | }.bind(this); |
655 | | |
656 | | // store old std. handler |
657 | 0 | this.oldWrite = process.stdout.write; |
658 | 0 | this.oldWriteErr = process.stderr.write; |
659 | | |
660 | | // overwrite with ugliness |
661 | 0 | process.stdout.write = _supLogs; |
662 | 0 | process.stderr.write = _supLogs; |
663 | 0 | return this; |
664 | | }, |
665 | | |
666 | | /** |
667 | | * Reinstantiate stdout handler after appium has |
668 | | * been started |
669 | | * |
670 | | * @method _reinstantiateLog |
671 | | * @chainable |
672 | | * @private |
673 | | */ |
674 | | |
675 | | _reinstantiateLog: function () { |
676 | 0 | setTimeout(function () { |
677 | 0 | process.stdout.write = this.oldWrite; |
678 | 0 | process.stderr.write = this.oldWriteErr; |
679 | | }.bind(this), 8000); |
680 | 0 | return this; |
681 | | } |
682 | | |
683 | | }; |
684 | | |
685 | | // expose the module |
686 | 1 | module.exports = IosDriver; |
687 | | |