Skip to content

Commit 1981613

Browse files
authored
Add support for Appium. (#7)
1 parent 0039df4 commit 1981613

File tree

8 files changed

+927
-70
lines changed

8 files changed

+927
-70
lines changed

src/commands/android/constants.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ export const AVAILABLE_OPTIONS: AvailableOptions = {
1919
browsers: {
2020
alias: ['browser', 'b'],
2121
description: 'Browsers to setup on Android emulator. Available args: "chrome", "firefox", "both", "none"'
22+
},
23+
appium: {
24+
alias: [],
25+
description: 'Make sure the final setup works with Appium out-of-the-box.'
2226
}
2327
};
2428

@@ -44,17 +48,17 @@ export const SETUP_CONFIG_QUES: inquirer.QuestionCollection = [
4448
{
4549
type: 'list',
4650
name: 'mode',
47-
message: 'Where do you want to run the tests?',
51+
message: 'Select target device(s):',
4852
choices: [
49-
{name: 'On real Android device', value: 'real'},
50-
{name: 'On an Android Emulator', value: 'emulator'},
53+
{name: 'Real Android Device', value: 'real'},
54+
{name: 'Android Emulator', value: 'emulator'},
5155
{name: 'Both', value: 'both'}
5256
]
5357
},
5458
{
5559
type: 'list',
5660
name: 'browsers',
57-
message: '[Emulator] Which browser(s) should we set up on the Emulator?',
61+
message: '[Emulator] Select browser(s) to set up on Emulator:',
5862
choices: [
5963
{name: 'Google Chrome', value: 'chrome'},
6064
{name: 'Mozilla Firefox', value: 'firefox'},

src/commands/android/index.ts

Lines changed: 227 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,31 @@ import {
1919
downloadFirefoxAndroid, downloadWithProgressBar, getAllAvailableOptions,
2020
getBinaryLocation, getBinaryNameForOS, getFirefoxApkName, getLatestVersion
2121
} from './utils/common';
22-
import {downloadAndSetupAndroidSdk, execBinarySync, getDefaultAndroidSdkRoot, installPackagesUsingSdkManager} from './utils/sdk';
22+
import {
23+
downloadAndSetupAndroidSdk, downloadSdkBuildTools, execBinarySync,
24+
getBuildToolsAvailableVersions, getDefaultAndroidSdkRoot, installPackagesUsingSdkManager
25+
} from './utils/sdk';
2326

2427
import DOWNLOADS from './downloads.json';
2528

2629

2730
export class AndroidSetup {
2831
sdkRoot: string;
32+
javaHome: string;
2933
options: Options;
3034
rootDir: string;
3135
platform: Platform;
3236
otherInfo: OtherInfo;
3337

34-
constructor(options: Options, rootDir = process.cwd()) {
38+
constructor(options: Options = {}, rootDir = process.cwd()) {
3539
this.sdkRoot = '';
40+
this.javaHome = '';
3641
this.options = options;
3742
this.rootDir = rootDir;
3843
this.platform = getPlatformName();
3944
this.otherInfo = {
40-
androidHomeInGlobalEnv: false
45+
androidHomeInGlobalEnv: false,
46+
javaHomeInGlobalEnv: false
4147
};
4248
}
4349

@@ -56,14 +62,52 @@ export class AndroidSetup {
5662
return false;
5763
}
5864

59-
let result = true;
65+
this.loadEnvFromDotEnv();
66+
67+
let javaHomeFound: boolean | null = false;
68+
if (this.options.appium) {
69+
javaHomeFound = this.isJavaHomeEnvSet();
70+
71+
if (!javaHomeFound) {
72+
this.javaHomeNotFoundInstructions();
73+
74+
if (javaHomeFound === false) {
75+
Logger.log(`${colors.red(
76+
'ERROR:'
77+
)} JAVA_HOME env variable could not be set in a .env file. Please set the JAVA_HOME env variable as instructed above.`);
78+
79+
this.envSetHelp();
80+
81+
return false;
82+
}
83+
84+
// env can be set in dotenv file (javaHomeFound=null).
85+
process.env.JAVA_HOME = await this.getJavaHomeFromUser();
86+
}
87+
88+
this.javaHome = process.env.JAVA_HOME || '';
89+
}
6090

6191
const sdkRootEnv = this.getSdkRootFromEnv();
62-
this.sdkRoot = sdkRootEnv || await this.getSdkRootFromUser();
6392

64-
const originalAndroidHome = process.env.ANDROID_HOME;
93+
if (this.options.appium && !sdkRootEnv && this.otherInfo.androidHomeInGlobalEnv) {
94+
// ANDROID_HOME is set to an invalid path in system env. We can get around this for mobile-web
95+
// since ANDROID_HOME is not a mandatory requirement there and Nightwatch would complain when it
96+
// is required, but for Appium to work properly, it should be set to correct path in sys env or .env.
97+
Logger.log(`${colors.red('ERROR:')} For Appium to work properly, ${colors.cyan(
98+
'ANDROID_HOME'
99+
)} env variable must be set to a valid path in your system environment variables.`);
100+
101+
this.envSetHelp();
102+
103+
return false;
104+
}
105+
106+
this.sdkRoot = sdkRootEnv || await this.getSdkRootFromUser();
65107
process.env.ANDROID_HOME = this.sdkRoot;
66108

109+
let result = true;
110+
67111
const setupConfigs: SetupConfigs = await this.getSetupConfigs(this.options);
68112
Logger.log();
69113

@@ -94,10 +138,8 @@ export class AndroidSetup {
94138
Logger.log(`${colors.bold('Note:')} Please make sure you have required browsers installed on your real-device before running tests.\n`);
95139
}
96140

97-
process.env.ANDROID_HOME = originalAndroidHome;
98-
99-
if (!sdkRootEnv) {
100-
this.sdkRootEnvSetInstructions();
141+
if (!sdkRootEnv || (this.options.appium && !javaHomeFound)) {
142+
this.envSetInstructions(sdkRootEnv);
101143
}
102144

103145
return {
@@ -162,7 +204,7 @@ export class AndroidSetup {
162204

163205
return true;
164206
} catch {
165-
Logger.log(`${colors.red('Error:')} Java Development Kit is required to work with Android SDKs. Download from here:`);
207+
Logger.log(`${colors.red('Error:')} Java Development Kit v9 or above is required to work with Android SDKs. Download from here:`);
166208
Logger.log(colors.cyan(' https://www.oracle.com/java/technologies/downloads/'), '\n');
167209

168210
Logger.log(`Make sure Java is installed by running ${colors.green('java -version')} command and then re-run this tool.\n`);
@@ -171,12 +213,119 @@ export class AndroidSetup {
171213
}
172214
}
173215

174-
getSdkRootFromEnv(): string {
175-
Logger.log('Checking the value of ANDROID_HOME environment variable...');
176-
216+
loadEnvFromDotEnv(): void {
177217
this.otherInfo.androidHomeInGlobalEnv = 'ANDROID_HOME' in process.env;
178218

219+
if (this.options.appium) {
220+
this.otherInfo.javaHomeInGlobalEnv = 'JAVA_HOME' in process.env;
221+
}
222+
179223
dotenv.config({path: path.join(this.rootDir, '.env')});
224+
}
225+
226+
isJavaHomeEnvSet(): boolean | null {
227+
Logger.log('Checking the value of JAVA_HOME environment variable...');
228+
229+
const javaHome = process.env.JAVA_HOME;
230+
const fromDotEnv = this.otherInfo.javaHomeInGlobalEnv ? '' : ' (taken from .env)';
231+
232+
if (javaHome !== undefined && fs.existsSync(javaHome)) {
233+
Logger.log(` ${colors.green(symbols().ok)} JAVA_HOME is set to '${javaHome}'${fromDotEnv}`);
234+
235+
const javaHomeBin = path.resolve(javaHome, 'bin');
236+
if (fs.existsSync(javaHomeBin)) {
237+
Logger.log(` ${colors.green(symbols().ok)} 'bin' subfolder exists under '${javaHome}'\n`);
238+
239+
return true;
240+
}
241+
242+
Logger.log(` ${colors.red(symbols().fail)} 'bin' subfolder does not exist under '${javaHome}'. Is ${javaHome} set to a proper value?\n`);
243+
if (this.otherInfo.javaHomeInGlobalEnv) {
244+
// we cannot set it ourselves
245+
return false;
246+
}
247+
248+
// We can set the env in dotenv file.
249+
return null;
250+
}
251+
252+
if (javaHome === undefined) {
253+
Logger.log(` ${colors.red(symbols().fail)} JAVA_HOME env variable is NOT set!\n`);
254+
} else {
255+
Logger.log(` ${colors.red(symbols().fail)} JAVA_HOME is set to '${javaHome}'${fromDotEnv} but this is NOT a valid path!\n`);
256+
257+
if (this.otherInfo.javaHomeInGlobalEnv) {
258+
// we cannot set it ourselves
259+
return false;
260+
}
261+
}
262+
263+
// we can set the env in dotenv file.
264+
return null;
265+
}
266+
267+
javaHomeNotFoundInstructions(): void {
268+
Logger.log(`${colors.red('NOTE:')} For Appium to work properly, ${colors.cyan(
269+
'JAVA_HOME'
270+
)} env variable must be set to the root folder path of your local JDK installation.`);
271+
272+
let expectedPath;
273+
274+
if (this.platform === 'windows') {
275+
expectedPath = 'C:\\Program Files\\Java\\jdk1.8.0_111';
276+
} else if (this.platform === 'mac') {
277+
expectedPath = '/Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home';
278+
} else {
279+
expectedPath = '/usr/lib/jvm/java-8-oracle';
280+
}
281+
282+
Logger.log(`${colors.green('Hint:')} On a ${this.platform} system, the JDK installation path should be something similar to '${expectedPath}'.\n`);
283+
}
284+
285+
async getJavaHomeFromUser(): Promise<string> {
286+
let javaHome;
287+
288+
if (this.platform === 'mac') {
289+
try {
290+
const stdout = execSync('/usr/libexec/java_home', {
291+
stdio: 'pipe'
292+
});
293+
294+
javaHome = stdout.toString();
295+
296+
Logger.log(`Auto-detected JAVA_HOME to be: ${colors.green(javaHome)}`);
297+
// eslint-disable-next-line
298+
} catch {}
299+
}
300+
301+
const answers: {javaHome: string} = await prompt([
302+
{
303+
type: 'input',
304+
name: 'javaHome',
305+
message: 'Enter the path to the root folder of your local JDK installation:',
306+
validate: (input) => {
307+
if (!fs.existsSync(input)) {
308+
return 'Entered path does not exist';
309+
}
310+
311+
if (!fs.existsSync(path.resolve(input, 'bin'))) {
312+
return `'bin' subfolder does not exist under '${input}'`;
313+
}
314+
315+
return true;
316+
}
317+
}
318+
], {javaHome});
319+
Logger.log();
320+
321+
const envPath = path.join(this.rootDir, '.env');
322+
fs.appendFileSync(envPath, `\nJAVA_HOME=${answers.javaHome}`);
323+
324+
return answers.javaHome;
325+
}
326+
327+
getSdkRootFromEnv(): string {
328+
Logger.log('Checking the value of ANDROID_HOME environment variable...');
180329

181330
const androidHome = process.env.ANDROID_HOME;
182331
const fromDotEnv = this.otherInfo.androidHomeInGlobalEnv ? '' : ' (taken from .env)';
@@ -228,7 +377,7 @@ export class AndroidSetup {
228377
// this is important if global ANDROID_HOME env is set to '', in which case we
229378
// should not save the user supplied value to .env.
230379
const envPath = path.join(this.rootDir, '.env');
231-
fs.appendFileSync(envPath, `\nANDROID_HOME=${sdkRoot}\n`);
380+
fs.appendFileSync(envPath, `\nANDROID_HOME=${sdkRoot}`);
232381
}
233382

234383
return sdkRoot;
@@ -406,6 +555,23 @@ export class AndroidSetup {
406555
const missingBinaries = this.checkBinariesPresent(requiredBinaries);
407556
missingRequirements.push(...missingBinaries);
408557

558+
// check for build-tools
559+
if (this.options.appium) {
560+
const buildToolsPath = path.join(this.sdkRoot, 'build-tools');
561+
const availableVersions = getBuildToolsAvailableVersions(buildToolsPath);
562+
if (availableVersions.length > 0) {
563+
Logger.log(
564+
` ${colors.green(symbols().ok)} ${colors.cyan('Android Build Tools')} present at '${buildToolsPath}'.`,
565+
`Available versions: ${colors.cyan(availableVersions.join(', '))}\n`
566+
);
567+
} else {
568+
Logger.log(
569+
` ${colors.red(symbols().fail)} ${colors.cyan('Android Build Tools')} not present at '${buildToolsPath}'\n`
570+
);
571+
missingRequirements.push('build-tools');
572+
}
573+
}
574+
409575
// check for platforms subdirectory (required by emulator)
410576
if (requiredBinaries.includes('emulator')) {
411577
const platormsPath = path.join(this.sdkRoot, 'platforms');
@@ -488,6 +654,17 @@ export class AndroidSetup {
488654
packagesToInstall
489655
);
490656

657+
// Download build-tools if using Appium
658+
if (this.options.appium) {
659+
const res = downloadSdkBuildTools(
660+
getBinaryLocation(this.sdkRoot, this.platform, 'sdkmanager', true),
661+
this.platform
662+
);
663+
if (!res) {
664+
result = false;
665+
}
666+
}
667+
491668
if (missingRequirements.includes('platforms')) {
492669
Logger.log('Creating platforms subdirectory...');
493670

@@ -814,25 +991,49 @@ export class AndroidSetup {
814991
}
815992
}
816993

817-
sdkRootEnvSetInstructions() {
994+
envSetInstructions(sdkRootEnv: string) {
818995
Logger.log(colors.red('IMPORTANT'));
819996
Logger.log(colors.red('---------'));
820997

821-
if (this.otherInfo.androidHomeInGlobalEnv && process.env.ANDROID_HOME === '') {
822-
Logger.log(`${colors.cyan('ANDROID_HOME')} env is set to '' which is NOT a valid path!\n`);
823-
Logger.log(`Please set ${colors.cyan('ANDROID_HOME')} to '${this.sdkRoot}' in your environment variables.`);
824-
Logger.log('(As ANDROID_HOME env is already set, temporarily saving it to .env won\'t work.)\n');
825-
} else {
998+
if (!sdkRootEnv) {
999+
// ANDROID_HOME is either undefined or '' in system env and .env.
1000+
if (this.otherInfo.androidHomeInGlobalEnv) {
1001+
// ANDROID_HOME is set in system env, to ''.
1002+
Logger.log(`${colors.cyan('ANDROID_HOME')} env is set to '' which is NOT a valid path!\n`);
1003+
Logger.log(`Please set ${colors.cyan('ANDROID_HOME')} to '${this.sdkRoot}' in your system environment variables.`);
1004+
Logger.log('(As ANDROID_HOME is already set in system env, temporarily saving it to .env file won\'t work.)\n');
1005+
} else {
1006+
Logger.log(
1007+
`${colors.cyan('ANDROID_HOME')} env was temporarily saved in ${colors.cyan(
1008+
'.env'
1009+
)} file (set to '${this.sdkRoot}').\n`
1010+
);
1011+
Logger.log(`Please set ${colors.cyan(
1012+
'ANDROID_HOME'
1013+
)} env to '${this.sdkRoot}' in your system environment variables and then delete it from ${colors.cyan('.env')} file.\n`);
1014+
}
1015+
}
1016+
1017+
if (this.options.appium) {
1018+
// JAVA_HOME env was not found and we set it ourselves in .env (javaHomeFound=null).
1019+
// In case JAVA_HOME was found incorrect in system env (javaHomeFound=false),
1020+
// process should have exited with error.
8261021
Logger.log(
827-
`${colors.cyan('ANDROID_HOME')} env was temporarily saved in ${colors.cyan(
1022+
`${colors.cyan('JAVA_HOME')} env was temporarily saved in ${colors.cyan(
8281023
'.env'
829-
)} file (set to '${this.sdkRoot}').\n`
1024+
)} file (set to '${this.javaHome}').\n`
8301025
);
8311026
Logger.log(`Please set ${colors.cyan(
832-
'ANDROID_HOME'
833-
)} env to '${this.sdkRoot}' globally and then delete it from ${colors.cyan('.env')} file.`);
1027+
'JAVA_HOME'
1028+
)} env to '${this.javaHome}' in your system environment variables and then delete it from ${colors.cyan('.env')} file.\n`);
8341029
}
8351030

836-
Logger.log('Doing this now might save you from future troubles.\n');
1031+
Logger.log('Following the above instructions might save you from future troubles.\n');
1032+
1033+
this.envSetHelp();
1034+
}
1035+
1036+
envSetHelp() {
1037+
// Add platform-wise help or link a doc to help users set env variable.
8371038
}
8381039
}

src/commands/android/interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type Platform = 'windows' | 'linux' | 'mac';
1919

2020
export interface OtherInfo {
2121
androidHomeInGlobalEnv: boolean;
22+
javaHomeInGlobalEnv: boolean;
2223
}
2324

2425
export interface SetupConfigs {

src/commands/android/utils/common.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const getBinaryNameForOS = (platform: Platform, binaryName: string) => {
2525
return binaryName;
2626
}
2727

28-
if (['sdkmanager', 'avdmanager'].includes(binaryName)) {
28+
if (['sdkmanager', 'avdmanager', 'apksigner'].includes(binaryName)) {
2929
return `${binaryName}.bat`;
3030
}
3131

0 commit comments

Comments
 (0)