| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988 |
1x
1x
1x
1x
1x
1x
1x
29x
29x
29x
29x
29x
29x
29x
29x
29x
29x
4x
4x
60x
60x
58x
2x
2x
2x
2x
6x
6x
52x
52x
6x
8x
8x
8x
8x
8x
2x
6x
12x
12x
12x
2x
2x
2x
2x
4x
4x
2x
2x
4x
4x
4x
2x
2x
15x
11x
11x
11x
6x
8x
8x
17x
17x
17x
15x
15x
6x
6x
4x
2x
15x
15x
2x
15x
2x
15x
15x
15x
15x
12x
12x
15x
4x
4x
| import path from 'path';
import * as simctl from 'node-simctl';
import { default as xcode, getPath as getXcodePath } from 'appium-xcode';
import log from './logger';
import { fs } from 'appium-support';
import B from 'bluebird';
import _ from 'lodash';
import { killAllSimulators, safeRimRaf } from './utils.js';
import { setTouchEnrollKey } from './touch-enroll.js';
import { asyncmap, retryInterval, waitForCondition, retry } from 'asyncbox';
import * as settings from './settings';
import { exec } from 'teen_process';
import { tailUntil } from './tail-until.js';
import extensions from './extensions/index';
import events from 'events';
import Calendar from './calendar';
const { EventEmitter } = events;
const STARTUP_TIMEOUT = 60 * 1000;
const EXTRA_STARTUP_TIME = 2000;
/*
* This event is emitted as soon as iOS Simulator
* has finished booting and it is ready to accept xcrun commands.
* The event handler is called after 'run' method is completed
* for Xcode 7 and older and is only useful in Xcode 8+,
* since one can start doing stuff (for example install/uninstall an app) in parallel
* with Simulator UI startup, which shortens session startup time.
*/
const BOOT_COMPLETED_EVENT = 'bootCompleted';
class SimulatorXcode6 extends EventEmitter {
/**
* Constructs the object with the `udid` and version of Xcode. Use the exported `getSimulator(udid)` method instead.
*
* @param {string} udid - The Simulator ID.
* @param {object} xcodeVersion - The target Xcode version in format {major, minor, build}.
*/
constructor (udid, xcodeVersion) {
super();
this.udid = String(udid);
this.xcodeVersion = xcodeVersion;
// platformVersion cannot be found initially, since getting it has side effects for
// our logic for figuring out if a sim has been run
// it will be set when it is needed
this._platformVersion = null;
this.keychainPath = path.resolve(this.getDir(), 'Library', 'Keychains');
this.simulatorApp = 'iOS Simulator.app';
this.appDataBundlePaths = {};
// list of files to check for when seeing if a simulator is "fresh"
// (meaning it has never been booted).
// If these files are present, we assume it's been successfully booted
this.isFreshFiles = [
'Library/ConfigurationProfiles',
'Library/Cookies',
'Library/Preferences/.GlobalPreferences.plist',
'Library/Preferences/com.apple.springboard.plist',
'var/run/syslog.pid'
];
// extra time to wait for simulator to be deemed booted
this.extraStartupTime = EXTRA_STARTUP_TIME;
this.calendar = new Calendar(this.getDir());
}
/**
* Check the state of Simulator UI client.
*
* @return {boolean} True of if UI client is running or false otherwise.
*/
async isUIClientRunning () {
try {
await exec('pgrep', ['-x', this.simulatorApp.split('.')[0]]);
return true;
} catch (err) {
return false;
}
}
/**
* How long to wait before throwing an error about Simulator startup timeout happened.
*
* @return {number} The number of milliseconds.
*/
get startupTimeout () {
return STARTUP_TIMEOUT;
}
/**
* Get the platform version of the current Simulator.
*
* @return {string} SDK version, for example '8.3'.
*/
async getPlatformVersion () {
if (!this._platformVersion) {
let {sdk} = await this.stat();
this._platformVersion = sdk;
}
return this._platformVersion;
}
/**
* Retrieve the full path to the directory where Simulator stuff is located.
*
* @return {string} The path string.
*/
getRootDir () {
let home = process.env.HOME;
return path.resolve(home, 'Library', 'Developer', 'CoreSimulator', 'Devices');
}
/**
* Retrieve the full path to the directory where Simulator applications data is located.
*
* @return {string} The path string.
*/
getDir () {
return path.resolve(this.getRootDir(), this.udid, 'data');
}
/**
* Retrieve the full path to the directory where Simulator logs are stored.
*
* @return {string} The path string.
*/
getLogDir () {
let home = process.env.HOME;
return path.resolve(home, 'Library', 'Logs', 'CoreSimulator', this.udid);
}
/**
* Install valid .app package on Simulator.
*
* @param {string} app - The path to the .app package.
*/
async installApp (app) {
return await simctl.installApp(this.udid, app);
}
/**
* Verify whether the particular application is installed on Simulator.
*
* @param {string} bundleId - The bundle id of the application to be checked.
* @param {string} appFule - Application name minus ".app" (for iOS 7.1)
* @return {boolean} True if the given application is installed
*/
async isAppInstalled (bundleId, appFile = null) {
// `appFile` argument only necessary for iOS below version 8
let appDirs = await this.getAppDirs(appFile, bundleId);
return appDirs.length !== 0;
}
/**
* Retrieve the directory for a particular application's data.
*
* @param {string} id - Either a bundleId (e.g., com.apple.mobilesafari) or, for iOS 7.1, the app name without `.app` (e.g., MobileSafari)
* @param {string} subdir - The sub-directory we expect to be within the application directory. Defaults to "Data".
* @return {string} The root application folder.
*/
async getAppDir (id, subDir = 'Data') {
this.appDataBundlePaths[subDir] = this.appDataBundlePaths[subDir] || {};
if (_.isEmpty(this.appDataBundlePaths[subDir]) && !await this.isFresh()) {
this.appDataBundlePaths[subDir] = await this.buildBundlePathMap(subDir);
}
return this.appDataBundlePaths[subDir][id];
}
/**
* The xcode 6 simulators are really annoying, and bury the main app
* directories inside directories just named with Hashes.
* This function finds the proper directory by traversing all of them
* and reading a metadata plist (Mobile Container Manager) to get the
* bundle id.
*
* @param {string} subdir - The sub-directory we expect to be within the application directory. Defaults to "Data".
* @return {object} The list of path-bundle pairs to an object where bundleIds are mapped to paths.
*/
async buildBundlePathMap (subDir = 'Data') {
log.debug('Building bundle path map');
let applicationList;
let pathBundlePair;
if (await this.getPlatformVersion() === '7.1') {
// apps available
// Web.app,
// WebViewService.app,
// MobileSafari.app,
// WebContentAnalysisUI.app,
// DDActionsService.app,
// StoreKitUIService.app
applicationList = path.resolve(this.getDir(), 'Applications');
pathBundlePair = async (dir) => {
dir = path.resolve(applicationList, dir);
let appFiles = await fs.glob(`${dir}/*.app`);
let bundleId = appFiles[0].match(/.*\/(.*)\.app/)[1];
return {path: dir, bundleId};
};
} else {
applicationList = path.resolve(this.getDir(), 'Containers', subDir, 'Application');
// given a directory, find the plist file and pull the bundle id from it
let readBundleId = async (dir) => {
let plist = path.resolve(dir, '.com.apple.mobile_container_manager.metadata.plist');
let metadata = await settings.read(plist);
return metadata.MCMMetadataIdentifier;
};
// given a directory, return the path and bundle id associated with it
pathBundlePair = async (dir) => {
dir = path.resolve(applicationList, dir);
let bundleId = await readBundleId(dir);
return {path: dir, bundleId};
};
}
let bundlePathDirs = await fs.readdir(applicationList);
let bundlePathPairs = await asyncmap(bundlePathDirs, async (dir) => {
return await pathBundlePair(dir);
}, false);
// reduce the list of path-bundle pairs to an object where bundleIds are mapped to paths
return bundlePathPairs.reduce((bundleMap, bundlePath) => {
bundleMap[bundlePath.bundleId] = bundlePath.path;
return bundleMap;
}, {});
}
/**
* Get the state and specifics of this sim.
*
* @return {object} Simulator stats mapping, for example:
* { name: 'iPhone 4s',
* udid: 'C09B34E5-7DCB-442E-B79C-AB6BC0357417',
* state: 'Shutdown',
* sdk: '8.3'
* }
*/
async stat () {
let devices = await simctl.getDevices();
let normalizedDevices = [];
// add sdk attribute to all entries, add to normalizedDevices
for (let [sdk, deviceArr] of _.toPairs(devices)) {
deviceArr = deviceArr.map((device) => {
device.sdk = sdk;
return device;
});
normalizedDevices = normalizedDevices.concat(deviceArr);
}
return _.find(normalizedDevices, {udid: this.udid});
}
/**
* This is a best-bet heuristic for whether or not a sim has been booted
* before. We usually want to start a simulator to "warm" it up, have
* Xcode populate it with plists for us to manipulate before a real
* test run.
*
* @return {boolean} True if the current Simulator has never been started before
*/
async isFresh () {
// if the following files don't exist, it hasn't been booted.
// THIS IS NOT AN EXHAUSTIVE LIST
log.debug('Checking whether simulator has been run before');
let files = this.isFreshFiles;
let pv = await this.getPlatformVersion();
if (pv !== '7.1') {
files.push('Library/Preferences/com.apple.Preferences.plist');
} else {
files.push('Applications');
}
files = files.map((s) => {
return path.resolve(this.getDir(), s);
});
let existences = await asyncmap(files, async (f) => { return await fs.hasAccess(f); });
let fresh = _.compact(existences).length !== files.length;
log.debug(`Simulator ${fresh ? 'has not' : 'has'} been run before`);
return fresh;
}
/**
* Retrieves the state of the current Simulator. One should distinguish the
* states of Simulator UI and the Simulator itself.
*
* @return {boolean} True if the current Simulator is running.
*/
async isRunning () {
let stat = await this.stat();
return stat.state === 'Booted';
}
/**
* Verify whether the Simulator booting is completed and/or wait for it
* until the timeout expires.
*
* @param {number} startupTimeout - the number of milliseconds to wait until booting is completed.
* @emits BOOT_COMPLETED_EVENT if the current Simulator is ready to accept simctl commands, like 'install'.
*/
async waitForBoot (startupTimeout) {
// wait for the simulator to boot
// waiting for the simulator status to be 'booted' isn't good enough
// it claims to be booted way before finishing loading
// let's tail the simulator system log until we see a magic line (this.bootedIndicator)
let bootedIndicator = await this.getBootedIndicatorString();
await this.tailLogsUntil(bootedIndicator, startupTimeout);
// so sorry, but we should wait another two seconds, just to make sure we've really started
// we can't look for another magic log line, because they seem to be app-dependent (not system dependent)
log.debug(`Waiting an extra ${this.extraStartupTime}ms for the simulator to really finish booting`);
await B.delay(this.extraStartupTime);
log.debug('Done waiting extra time for simulator');
this.emit(BOOT_COMPLETED_EVENT);
}
/**
* Returns a magic string, which, if present in logs, reflects the fact that simulator booting has been completed.
*
* @return {string} The magic log string.
*/
async getBootedIndicatorString () {
let indicator;
let platformVersion = await this.getPlatformVersion();
switch (platformVersion) {
case '7.1':
case '8.1':
case '8.2':
case '8.3':
case '8.4':
indicator = 'profiled: Service starting...';
break;
case '9.0':
case '9.1':
case '9.2':
case '9.3':
indicator = 'System app "com.apple.springboard" finished startup';
break;
case '10.0':
indicator = 'Switching to keyboard';
break;
default:
log.warn(`No boot indicator case for platform version '${platformVersion}'`);
indicator = 'no boot indicator string available';
}
return indicator;
}
/**
* Start the Simulator UI client with the given arguments
*
* @param {object} opts - One or more of available Simulator UI client options:
* - {string} scaleFactor: can be one of ['1.0', '0.75', '0.5', '0.33', '0.25'].
* Defines the window scale value for the UI client window for the current Simulator.
* Equals to null by default, which keeps the current scale unchanged.
* - {boolean} connectHardwareKeyboard: whether to connect the hardware keyboard to the
* Simulator UI client. Equals to false by default.
* - {number} startupTimeout: number of milliseconds to wait until Simulator booting
* process is completed. The default timeout will be used if not set explicitly.
*/
async startUIClient (opts = {}) {
opts = Object.assign({
scaleFactor: null,
connectHardwareKeyboard: false,
startupTimeout: this.startupTimeout,
}, opts);
let simulatorApp = path.resolve(await getXcodePath(), 'Applications', this.simulatorApp);
let args = ['-Fn', simulatorApp, '--args', '-CurrentDeviceUDID', this.udid];
if (opts.scaleFactor) {
const supportedScales = ['1.0', '0.75', '0.5', '0.33', '0.25'];
if (supportedScales.indexOf(opts.scaleFactor) < 0) {
log.errorAndThrow(`Only "${supportedScales}" values are supported as scale factors. "${opts.scaleFactor}" is passed instead.`);
}
const stat = await this.stat();
const formattedDeviceName = stat.name.replace(/\s+/g, '-');
const argumentName = `-SimulatorWindowLastScale-com.apple.CoreSimulator.SimDeviceType.${formattedDeviceName}`;
args.push(argumentName, opts.scaleFactor);
}
if (!opts.connectHardwareKeyboard) {
args.push('-ConnectHardwareKeyboard', '0');
}
log.info(`Starting Simulator UI with command: open ${args.join(' ')}`);
await exec('open', args, {timeout: opts.startupTimeout});
}
/**
* Executes given Simulator with options. The Simulator will not be restarted if
* it is already running.
*
* @param {object} opts - One or more of available Simulator options. Supported keys:
* - {boolean} allowTouchEnroll: whether to enroll Touch ID in the Simulator UI client.
* Equals to false by default.
*
* See {#startUIClient(opts)} documentation for more details on other supported keys.
*/
async run (opts = {}) {
opts = Object.assign({
allowTouchEnroll: false,
startupTimeout: this.startupTimeout,
}, opts);
const {state} = await this.stat();
const isServerRunning = state === 'Booted';
const isUIClientRunning = await this.isUIClientRunning();
if (isServerRunning && isUIClientRunning) {
log.info(`Both Simulator with UDID ${this.udid} and the UI client are currently running`);
return;
}
const startTime = process.hrtime();
try {
await this.shutdown();
} catch (err) {
log.warn(`Error on Simulator shutdown: ${err.message}`);
}
// Set the 'Touch ID Enroll' key bindings before the Simulator starts
if (opts.allowTouchEnroll) {
await setTouchEnrollKey();
}
await this.startUIClient(opts);
await this.waitForBoot(opts.startupTimeout);
log.info(`Simulator with UDID ${this.udid} booted in ${process.hrtime(startTime)[0]} seconds`);
}
// TODO keep keychains
/**
* Reset the current Simulator to the clean state.
*/
async clean () {
await this.endSimulatorDaemon();
log.info(`Cleaning simulator ${this.udid}`);
await simctl.eraseDevice(this.udid, 10000);
}
/**
* Scrub (delete the preferences and changed files) the particular application on Simulator.
*
* @param {string} appFile - Application name minus ".app".
* @param {string} appBundleId - Bundle identifier of the application.
* @return {array} Array of deletion promises.
*/
async scrubCustomApp (appFile, appBundleId) {
return await this.cleanCustomApp (appFile, appBundleId, true);
}
/**
* Clean/scrub the particular application on Simulator.
*
* @param {string} appFile - Application name minus ".app".
* @param {string} appBundleId - Bundle identifier of the application.
* @param {boolean} scrub - If `scrub` is false, we want to clean by deleting the app and all
* files associated with it. If `scrub` is true, we just want to delete the preferences and
* changed files.
* @return {array} Array of deletion promises.
*/
async cleanCustomApp (appFile, appBundleId, scrub = false) {
log.debug(`Cleaning app data files for '${appFile}', '${appBundleId}'`);
Eif (!scrub) {
log.debug(`Deleting app altogether`);
}
// get the directories to be deleted
let appDirs = await this.getAppDirs(appFile, appBundleId, scrub);
if (appDirs.length === 0) {
log.debug("Could not find app directories to delete. It is probably not installed");
return;
}
let deletePromises = [];
for (let dir of appDirs) {
log.debug(`Deleting directory: '${dir}'`);
deletePromises.push(fs.rimraf(dir));
}
if (await this.getPlatformVersion() >= 8) {
let relRmPath = `Library/Preferences/${appBundleId}.plist`;
let rmPath = path.resolve(this.getRootDir(), relRmPath);
log.debug(`Deleting file: '${rmPath}'`);
deletePromises.push(fs.rimraf(rmPath));
}
await B.all(deletePromises);
}
/**
* Retrieve paths to dirs where application data is stored. iOS 8+ stores app data in two places,
* and iOS 7.1 has only one directory
*
* @param {string} appFile - Application name minus ".app".
* @param {string} appBundleId - Bundle identifier of the application.
* @param {boolean} scrub - The `Bundle` directory has the actual app in it. If we are just scrubbing,
* we want this to stay. If we are cleaning we delete.
* @return {array} Array of application data paths.
*/
async getAppDirs (appFile, appBundleId, scrub = false) {
let dirs = [];
if (await this.getPlatformVersion() >= 8) {
let data = await this.getAppDir(appBundleId);
if (!data) return dirs; // eslint-disable-line curly
let bundle = !scrub ? await this.getAppDir(appBundleId, 'Bundle') : undefined;
for (let src of [data, bundle]) {
Eif (src) {
dirs.push(src);
}
}
} else {
let data = await this.getAppDir(appFile);
Iif (data) {
dirs.push(data);
}
}
return dirs;
}
/**
* Execute the Simulator in order to have the initial file structure created and shutdown it afterwards.
*
* @param {boolean} safari - Whether to execute mobile Safari after startup.
* @param {number} startupTimeout - How long to wait until Simulator booting is completed (in milliseconds).
*/
async launchAndQuit (safari = false, startupTimeout = this.startupTimeout) {
log.debug('Attempting to launch and quit the simulator, to create directory structure');
log.debug(`Will launch with Safari? ${safari}`);
await this.run(startupTimeout);
if (safari) {
await this.openUrl('http://www.appium.io');
}
// wait for the system to create the files we will manipulate
// need quite a high retry number, in order to accommodate iOS 7.1
// locally, 7.1 averages 8.5 retries (from 6 - 12)
// 8 averages 0.6 retries (from 0 - 2)
// 9 averages 14 retries
await retryInterval(20, 250, async () => {
if (await this.isFresh()) {
let msg = 'Simulator files not fully created. Waiting a bit';
log.debug(msg);
throw new Error(msg);
}
});
// and quit
await this.shutdown();
}
/**
* Looks for launchd daemons corresponding to the sim udid and tries to stop them cleanly
* This prevents xcrun simctl erase from hanging.
*/
async endSimulatorDaemon () {
log.debug(`Killing any simulator daemons for ${this.udid}`);
let launchctlCmd = `launchctl list | grep ${this.udid} | cut -f 3 | xargs -n 1 launchctl`;
try {
let stopCmd = `${launchctlCmd} stop`;
await exec('bash', ['-c', stopCmd]);
} catch (err) {
log.warn(`Could not stop simulator daemons: ${err.message}`);
log.debug('Carrying on anyway!');
}
try {
let removeCmd = `${launchctlCmd} remove`;
await exec('bash', ['-c', removeCmd]);
} catch (err) {
log.warn(`Could not remove simulator daemons: ${err.message}`);
log.debug('Carrying on anyway!');
}
try {
// Waits 10 sec for the simulator launchd services to stop.
await waitForCondition(async () => {
let {stdout} = await exec('bash', ['-c',
`ps -e | grep ${this.udid} | grep launchd_sim | grep -v bash | grep -v grep | awk {'print$1'}`]);
return stdout.trim().length === 0;
}, {waitMs: 10000, intervalMs: 500});
} catch (err) {
log.warn(`Could not end simulator daemon for ${this.udid}: ${err.message}`);
log.debug('Carrying on anyway!');
}
}
/**
* Shutdown all the running Simulators and the UI client.
*/
async shutdown () {
await killAllSimulators();
}
/**
* Delete the particular Simulator from devices list
*/
async delete () {
await simctl.deleteDevice(this.udid);
}
/**
* Update the particular preference file with the given key/value pairs.
*
* @param {string} plist - The preferences file to update.
* @param {object} updates - The key/value pairs to update.
*/
async updateSettings (plist, updates) {
return await settings.updateSettings(this, plist, updates);
}
/**
* Authorize/de-authorize location settings for a particular application.
*
* @param {string} bundleId - The application ID to update.
* @param {boolean} authorized - Whether or not to authorize.
*/
async updateLocationSettings (bundleId, authorized) {
return await settings.updateLocationSettings(this, bundleId, authorized);
}
/**
* Update settings for Safari.
*
* @param {object} updates - The hash of key/value pairs to update for Safari.
*/
async updateSafariSettings (updates) {
await settings.updateSafariUserSettings(this, updates);
await settings.updateSettings(this, 'mobileSafari', updates);
}
/**
* Update the locale for the Simulator.
*
* @param {string} language - The language for the simulator. E.g., `"fr_US"`.
* @param {string} locale - The locale to set for the simulator. E.g., `"en"`.
* @param {string} calendarFormat - The format of the calendar.
*/
async updateLocale (language, locale, calendarFormat) {
return await settings.updateLocale(this, language, locale, calendarFormat);
}
/**
* Completely delete mobile Safari application from the current Simulator.
*/
async deleteSafari () {
log.debug('Deleting Safari apps from simulator');
let dirs = [];
// get the data directory
dirs.push(await this.getAppDir('com.apple.mobilesafari'));
let pv = await this.getPlatformVersion();
if (pv >= 8) {
// get the bundle directory
dirs.push(await this.getAppDir('com.apple.mobilesafari', 'Bundle'));
}
let deletePromises = [];
for (let dir of _.compact(dirs)) {
log.debug(`Deleting directory: '${dir}'`);
deletePromises.push(fs.rimraf(dir));
}
await B.all(deletePromises);
}
/**
* Clean up the directories for mobile Safari.
*
* @param {boolean} keepPrefs - Whether to keep Safari preferences from being deleted.
*/
async cleanSafari (keepPrefs = true) {
log.debug('Cleaning mobile safari data files');
if (await this.isFresh()) {
log.info('Could not find Safari support directories to clean out old ' +
'data. Probably there is nothing to clean out');
return;
}
let libraryDir = path.resolve(this.getDir(), 'Library');
let safariRoot = await this.getAppDir('com.apple.mobilesafari');
if (!safariRoot) {
log.info('Could not find Safari support directories to clean out old ' +
'data. Probably there is nothing to clean out');
return;
}
let safariLibraryDir = path.resolve(safariRoot, 'Library');
let filesToDelete = [
'Caches/Snapshots/com.apple.mobilesafari',
'Caches/com.apple.mobilesafari/*',
'Caches/com.apple.WebAppCache/*',
'Caches/com.apple.WebKit.Networking/*',
'Caches/com.apple.WebKit.WebContent/*',
'Image Cache/*',
'WebKit/com.apple.mobilesafari/*',
'WebKit/GeolocationSites.plist',
'WebKit/LocalStorage/*.*',
'Safari/*',
'Cookies/*.binarycookies',
'Caches/com.apple.UIStatusBar/*',
'Caches/com.apple.keyboards/images/*',
'Caches/com.apple.Safari.SafeBrowsing/*',
'../tmp/com.apple.mobilesafari/*'
];
let deletePromises = [];
for (let file of filesToDelete) {
deletePromises.push(fs.rimraf(path.resolve(libraryDir, file)));
deletePromises.push(fs.rimraf(path.resolve(safariLibraryDir, file)));
}
if (!keepPrefs) {
deletePromises.push(fs.rimraf(path.resolve(safariLibraryDir, 'Preferences/*.plist')));
}
await B.all(deletePromises);
}
/**
* Uninstall the given application from the current Simulator.
*
* @param {string} bundleId - The buindle ID of the application to be removed.
*/
async removeApp (bundleId) {
await simctl.removeApp(this.udid, bundleId);
}
/**
* Move a built-in application to a new place (actually, rename it).
*
* @param {string} appName - The name of the app to be moved.
* @param {string} appPath - The current path to the application.
* @param {string} newAppPath - The new path to the application.
* If some application already exists by this path then it's going to be removed.
*/
async moveBuiltInApp (appName, appPath, newAppPath) {
await safeRimRaf(newAppPath);
await fs.copyFile(appPath, newAppPath);
log.debug(`Copied '${appName}' to '${newAppPath}'`);
await fs.rimraf(appPath);
log.debug(`Temporarily deleted original app at '${appPath}'`);
return [newAppPath, appPath];
}
/**
* Open the given URL in mobile Safari browser.
* The browser will be started automatically if it is not running.
*
* @param {string} url - The URL to be opened.
*/
async openUrl (url) {
const SAFARI_BOOTED_INDICATOR = 'MobileSafari[';
const SAFARI_STARTUP_TIMEOUT = 15 * 1000;
const EXTRA_STARTUP_TIME = 3 * 1000;
if (await this.isRunning()) {
await retry(5000, simctl.openUrl, this.udid, url);
await this.tailLogsUntil(SAFARI_BOOTED_INDICATOR, SAFARI_STARTUP_TIMEOUT);
// So sorry, but the logs have nothing else for Safari starting.. just delay a little bit
log.debug(`Safari started, waiting ${EXTRA_STARTUP_TIME}ms for it to fully start`);
await B.delay(EXTRA_STARTUP_TIME);
log.debug('Done waiting for Safari');
return;
} else {
throw new Error('Tried to open a url, but the Simulator is not Booted');
}
}
/**
* Blocks until the given indicater string appears in Simulator logs.
*
* @param {string} bootedIndicator - The magic string, which appears in logs after Simulator booting is completed.
* @param {number} timeoutMs - The maximumm number of milliseconds to wait for the string indicator presence.
* @returns {Promise} A promise that resolves when the ios simulator logs output a line matching `bootedIndicator`
* times out after timeoutMs
*/
async tailLogsUntil (bootedIndicator, timeoutMs) {
let simLog = path.resolve(this.getLogDir(), 'system.log');
// we need to make sure log file exists before we can tail it
await retryInterval(200, 200, async () => {
let exists = await fs.exists(simLog);
if (!exists) {
throw new Error(`Could not find Simulator log: '${simLog}'`);
}
});
log.info(`Simulator log at '${simLog}'`);
log.info(`Tailing simulator logs until we encounter the string "${bootedIndicator}"`);
log.info(`We will time out after ${timeoutMs}ms`);
try {
await tailUntil(simLog, bootedIndicator, timeoutMs);
} catch (err) {
log.debug('Simulator startup timed out. Continuing anyway.');
}
}
/**
* Enable Calendar access for the given application.
*
* @param {string} bundleID - Bundle ID of the application, for which the access should be granted.
*/
async enableCalendarAccess (bundleID) {
await this.calendar.enableCalendarAccess(bundleID);
}
/**
* Disable Calendar access for the given application.
*
* @param {string} bundleID - Bundle ID of the application, for which the access should be denied.
*/
async disableCalendarAccess (bundleID) {
await this.calendar.disableCalendarAccess(bundleID);
}
/**
* Check whether the given application has access to Calendar.
*
* @return {boolean} True if the given application has the access.
*/
async hasCalendarAccess (bundleID) {
return await this.calendar.hasCalendarAccess(bundleID);
}
/**
* Execute a special Apple script, which enables Touch ID feature testing in Simulator UI client.
*/
async enrollTouchID () {
await exec('osascript', ['-e', `
activate application "Simulator"
tell application "System Events"
key code 17 using {control down, shift down, option down, command down}
end tell
`]);
}
/**
* Execute a special Apple script, which clicks the particular button on Database alert.
*
* @param {boolean} increase - Click the button with 'Increase' title on the alert if this
* parameter is true. The 'Cancel' button will be clicked otherwise.
*/
async dismissDatabaseAlert (increase = true) {
let button = increase ? 'Increase' : 'Cancel';
log.debug(`Attempting to dismiss database alert with '${button}' button`);
await exec('osascript', ['-e', `
activate application "Simulator"
tell application "System Events"
tell process "Simulator"
click button "${button}" of window 1
end tell
end tell
`]);
}
static async _getDeviceStringPlatformVersion (platformVersion) {
let reqVersion = platformVersion;
if (!reqVersion) {
reqVersion = await xcode.getMaxIOSSDK();
log.warn(`No platform version set. Using max SDK version: ${reqVersion}`);
// this will be a number, and possibly an integer (e.g., if max iOS SDK is 9)
// so turn it into a string and add a .0 if necessary
if (!_.isString(reqVersion)) {
reqVersion = (reqVersion % 1) ? String(reqVersion) : `${reqVersion}.0`;
}
}
return reqVersion;
}
// change the format in subclasses, as necessary
static async _getDeviceStringVersionString (platformVersion) {
let reqVersion = await this._getDeviceStringPlatformVersion(platformVersion);
return `(${reqVersion} Simulator)`;
}
// change the format in subclasses, as necessary
static _getDeviceStringConfigFix () {
// some devices need to be updated
return {
'iPad Simulator (7.1 Simulator)': 'iPad 2 (7.1 Simulator)',
'iPad Simulator (8.0 Simulator)': 'iPad 2 (8.0 Simulator)',
'iPad Simulator (8.1 Simulator)': 'iPad 2 (8.1 Simulator)',
'iPad Simulator (8.2 Simulator)': 'iPad 2 (8.2 Simulator)',
'iPad Simulator (8.3 Simulator)': 'iPad 2 (8.3 Simulator)',
'iPad Simulator (8.4 Simulator)': 'iPad 2 (8.4 Simulator)',
'iPhone Simulator (7.1 Simulator)': 'iPhone 5s (7.1 Simulator)',
'iPhone Simulator (8.4 Simulator)': 'iPhone 6 (8.4 Simulator)',
'iPhone Simulator (8.3 Simulator)': 'iPhone 6 (8.3 Simulator)',
'iPhone Simulator (8.2 Simulator)': 'iPhone 6 (8.2 Simulator)',
'iPhone Simulator (8.1 Simulator)': 'iPhone 6 (8.1 Simulator)',
'iPhone Simulator (8.0 Simulator)': 'iPhone 6 (8.0 Simulator)'
};
}
/**
* Takes a set of options and finds the correct device string in order for Instruments to
* identify the correct simulator.
*
* @param {object} opts - The options available are:
* - `deviceName` - a name for the device. If the given device name starts with `=`, the name, less the equals sign, is returned.
* - `platformVersion` - the version of iOS to use. Defaults to the current Xcode's maximum SDK version.
* - `forceIphone` - force the configuration of the device string to iPhone. Defaults to `false`.
* - `forceIpad` - force the configuration of the device string to iPad. Defaults to `false`.
* If both `forceIphone` and `forceIpad` are true, the device will be forced to iPhone.
*
* @return {string} The found device string.
*/
static async getDeviceString (opts) {
opts = Object.assign({}, {
deviceName: null,
platformVersion: null,
forceIphone: false,
forceIpad: false
}, opts);
let logOpts = {
deviceName: opts.deviceName,
platformVersion: opts.platformVersion,
forceIphone: opts.forceIphone,
forceIpad: opts.forceIpad
};
log.debug(`Getting device string from options: ${JSON.stringify(logOpts)}`);
// short circuit if we already have a device name
if ((opts.deviceName || '')[0] === '=') {
return opts.deviceName.substring(1);
}
let isiPhone = !!opts.forceIphone || !opts.forceIpad;
if (opts.deviceName) {
let device = opts.deviceName.toLowerCase();
if (device.indexOf('iphone') !== -1) {
isiPhone = true;
} else Iif (device.indexOf('ipad') !== -1) {
isiPhone = false;
}
}
let iosDeviceString = opts.deviceName || (isiPhone ? 'iPhone Simulator' : 'iPad Simulator');
// if someone passes in just "iPhone", make that "iPhone Simulator" to
// conform to all the logic below
if (/^(iPhone|iPad)$/.test(iosDeviceString)) {
iosDeviceString += " Simulator";
}
// we support deviceName: "iPhone Simulator", and also want to support
// "iPhone XYZ Simulator", but these strings aren't in the device list.
// So, if someone sent in "iPhone XYZ Simulator", strip off " Simulator"
// in order to allow the default "iPhone XYZ" match
if (/[^(iPhone|iPad)] Simulator/.test(iosDeviceString)) {
iosDeviceString = iosDeviceString.replace(" Simulator", "");
}
iosDeviceString += ` ${await this._getDeviceStringVersionString(opts.platformVersion)}`;
let CONFIG_FIX = this._getDeviceStringConfigFix();
let configFix = CONFIG_FIX;
if (configFix[iosDeviceString]) {
iosDeviceString = configFix[iosDeviceString];
log.debug(`Fixing device. Changed from '${opts.deviceName}' `+
`to '${iosDeviceString}'`);
}
log.debug(`Final device string is '${iosDeviceString}'`);
return iosDeviceString;
}
}
for (let [cmd, fn] of _.toPairs(extensions)) {
SimulatorXcode6.prototype[cmd] = fn;
}
export default SimulatorXcode6;
export { SimulatorXcode6, BOOT_COMPLETED_EVENT };
|