Coverage

7%
107
8
99

/home/ubuntu/src/github.com/dalekjs/dalek-browser-ios/index.js

7%
107
8
99
LineHitsSource
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
251'use strict';
26
27// ext. libs
281var Q = require('q');
291var os = require('os');
301var cp = require('child_process');
311var appium = require('appium/lib/server/main');
321var 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
941var 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 () {
2460 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 () {
2570 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 () {
2680 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 () {
2790 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 () {
2900 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) {
3050 var deferred = Q.defer();
306
307 // store injected configuration/log event handlers
3080 this.reporterEvents = events;
3090 this.configuration = configuration;
3100 this.config = config;
311
312 // check if the user wants to run the iPad emulator
3130 if (configuration && configuration.type === 'ipad') {
3140 this.longName = this.longName.replace('iPhone', 'iPad');
3150 this.appiumArgs.forceIpad = true;
316 }
317
318 // check for a user set port
3190 var browsers = this.config.get('browsers');
3200 if (browsers && Array.isArray(browsers)) {
3210 browsers.forEach(this._checkUserDefinedPorts.bind(this));
322 }
323
324 // check if the current port is in use, if so, scan for free ports
3250 portscanner.findAPortNotInUse(this.getPort(), this.getMaxPort(), this.getHost(), this._checkPorts.bind(this, deferred));
3260 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
3410 this.appiumServer.webSocket.server.close();
3420 this.appiumServer.rest.listen().close();
343 // slight timeout for process killing
3440 setTimeout(this._processes.bind(this, this._kill.bind(this)), 1000);
3450 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
3610 result.forEach(this._killProcess.bind(this));
362 // (re)establish stderr/stdout stream
3630 this._reinstantiateLog();
3640 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) {
3780 var kill = true;
379
380 // walk through the list of processes that are
381 // open before the driver started
3820 this.openProcesses.forEach(function (pid) {
3830 if (pid === processID) {
3840 kill = false;
385 }
386 });
387
3880 if (kill === true) {
3890 cp.spawn('kill', [processID]);
390 }
391
3920 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
4090 if (this.port !== port) {
4100 this.reporterEvents.emit('report:log:system', 'dalek-browser-ios: Switching to port: ' + port);
4110 this.port = port;
412 }
413
414 // check if the current webhook port is in use, if so, scan for free ports
4150 portscanner.findAPortNotInUse(this.getWebhookPort(), this.getMaxWebhookPort(), this.getHost(), this._launch.bind(this, deferred));
4160 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
4330 if (this.webhookPort !== port) {
4340 this.reporterEvents.emit('report:log:system', 'dalek-browser-ios: Switching to webhook port: ' + port);
4350 this.webhookPort = port;
436 }
437
438 // launch appium & the emulator
4390 this._processes(this._listProcesses.bind(this, deferred));
4400 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
4580 this.openProcesses = result;
459 // nasty hack to surpress socket.io debug reports from appium
4600 this._suppressAppiumLogs();
461 // run appium
4620 appium.run(this._loadAppiumArgs(this.appiumArgs), this._afterAppiumStarted.bind(this, deferred));
4630 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) {
4780 this.appiumServer = appiumServer;
4790 deferred.resolve();
4800 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) {
4930 appiumArgs.port = this.getPort();
4940 appiumArgs.webhook = this.getHost() + ':' + this.getWebhookPort();
4950 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) {
5080 this._checkAppiumPorts(browser);
5090 this._checkWebhookPorts(browser);
5100 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
5240 if (browser.ios && browser.ios.port) {
5250 this.port = parseInt(browser.ios.port, 10);
5260 this.maxPort = this.port + 90;
5270 this.reporterEvents.emit('report:log:system', 'dalek-browser-ios: Switching to user defined port: ' + this.port);
528 }
529
530 // check for a port range
5310 if (browser.ios && browser.ios.portRange && browser.ios.portRange.length === 2) {
5320 this.port = parseInt(browser.ios.portRange[0], 10);
5330 this.maxPort = parseInt(browser.ios.portRange[1], 10);
5340 this.reporterEvents.emit('report:log:system', 'dalek-browser-ios: Switching to user defined port(s): ' + this.port + ' -> ' + this.maxPort);
535 }
536
5370 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
5510 if (browser.ios && browser.ios.webhookPort) {
5520 this.webhookPort = parseInt(browser.ios.webhookPort, 10);
5530 this.maxWebhookPort = this.webhookPort + 90;
5540 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
5580 if (browser.ios && browser.ios.webhookPortRange && browser.ios.webhookPortRange.length === 2) {
5590 this.webhookPort = parseInt(browser.ios.webhookPortRange[0], 10);
5600 this.maxWebhookPort = parseInt(browser.ios.webhookPortRange[1], 10);
5610 this.reporterEvents.emit('report:log:system', 'dalek-browser-ios: Switching to user defined webhook port(s): ' + this.webhookPort + ' -> ' + this.maxWebhookPort);
562 }
563
5640 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) {
5770 var cmd = ['ps -ax', '|', 'grep "iPhone Simulator.app"'];
5780 cp.exec(cmd.join(' '), this._transformProcesses.bind(this, fn));
5790 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){
5950 var result = [];
5960 stdout.split('\n').forEach(this._scanProcess.bind(this, result));
5970 fn(err, result);
5980 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){
6120 var data = line.split(' ');
6130 data = data.filter(this._filterProcessItem);
614
6150 if (data[1] === '??') {
6160 result.push(data[0]);
617 }
618
6190 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) {
6320 if (item !== '') {
6330 return item;
634 }
635
6360 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
6500 var _supLogs = function (data) {
6510 if (data.search('6minfo') === -1 && data.search('33mwarn') === -1 && data.search('90mdebug') === -1) {
6520 this.oldWrite.bind(process.stdout)(data);
653 }
654 }.bind(this);
655
656 // store old std. handler
6570 this.oldWrite = process.stdout.write;
6580 this.oldWriteErr = process.stderr.write;
659
660 // overwrite with ugliness
6610 process.stdout.write = _supLogs;
6620 process.stderr.write = _supLogs;
6630 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 () {
6760 setTimeout(function () {
6770 process.stdout.write = this.oldWrite;
6780 process.stderr.write = this.oldWriteErr;
679 }.bind(this), 8000);
6800 return this;
681 }
682
683};
684
685// expose the module
6861module.exports = IosDriver;
687