Skip to content

Commit

Permalink
Merge branch 'main' into uninstall-app
Browse files Browse the repository at this point in the history
  • Loading branch information
garg3133 authored Aug 23, 2024
2 parents 0854eaa + d7d9a81 commit 553fa4b
Show file tree
Hide file tree
Showing 21 changed files with 956 additions and 59 deletions.
49 changes: 47 additions & 2 deletions src/commands/android/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import inquirer from 'inquirer';
import path from 'path';
import os from 'os';

import {AvailableOptions, AvailableSubcommands, SdkBinary} from './interfaces';
import {AvailableOptions, SdkBinary} from './interfaces';
import {AvailableSubcommands} from './subcommands/interfaces';

export const AVAILABLE_OPTIONS: AvailableOptions = {
help: {
Expand Down Expand Up @@ -34,12 +35,56 @@ export const AVAILABLE_OPTIONS: AvailableOptions = {
export const AVAILABLE_SUBCOMMANDS: AvailableSubcommands = {
connect: {
description: 'Connect to a device',
options: [
flags: [
{
name: 'wireless',
description: 'Connect a real device wirelessly'
}
]
},
list: {
description: 'List connected devices or installed AVDs',
flags: [{
name: 'device',
description: 'List connected devices (real devices and AVDs)'
},
{
name: 'avd',
description: 'List installed AVDs'
}]
},
install: {
description: 'Install APK or AVD on a device',
flags: [
{
name: 'avd',
description: 'Create an Android Virtual Device'
},
{
name: 'app',
description: 'Install an APK on the device',
cliConfigs: [
{
name: 'path',
alias: ['p'],
description: 'Path to the APK file',
usageHelp: 'path_to_apk'
},
{
name: 'deviceId',
alias: ['s'],
description: 'Id of the device to install the APK',
usageHelp: 'device_id'
}
]
}
]
},
uninstall: {
description: 'todo item',
flags: [
{name: 'avd', description: 'todo item'},
]
}
};

Expand Down
96 changes: 96 additions & 0 deletions src/commands/android/dotcommands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import colors from 'ansi-colors';
import {spawnSync} from 'child_process';
import * as dotenv from 'dotenv';
import path from 'path';

import {ANDROID_DOTCOMMANDS} from '../../constants';
import Logger from '../../logger';
import {getPlatformName} from '../../utils';
import {Platform, SdkBinary} from './interfaces';
import {checkJavaInstallation, getBinaryLocation, getBinaryNameForOS, getSdkRootFromEnv} from './utils/common';

export class AndroidDotCommand {
dotcmd: string;
args: string[];
sdkRoot: string;
rootDir: string;
platform: Platform;
androidHomeInGlobalEnv: boolean;

constructor(dotcmd: string, argv: string[], rootDir = process.cwd()) {
this.dotcmd = dotcmd;
this.args = argv.slice(1);
this.sdkRoot = '';
this.rootDir = rootDir;
this.platform = getPlatformName();
this.androidHomeInGlobalEnv = false;
}

async run(): Promise<boolean> {
if (!ANDROID_DOTCOMMANDS.includes(this.dotcmd)) {
Logger.log(colors.red(`Unknown dot command passed: ${this.dotcmd}\n`));

Logger.log('Run Android SDK command line tools using the following command:');
Logger.log(colors.cyan('npx @nightwatch/mobile-helper <DOTCMD> [options|args]\n'));

Logger.log(`Available Dot Commands: ${colors.magenta(ANDROID_DOTCOMMANDS.join(', '))}`);
Logger.log(`(Example command: ${colors.gray('npx @nightwatch/mobile-helper android.emulator @nightwatch-android-11')})\n`);

return false;
}

const javaInstalled = checkJavaInstallation(this.rootDir);
if (!javaInstalled) {
return false;
}

this.loadEnvFromDotEnv();

const sdkRootEnv = getSdkRootFromEnv(this.rootDir, this.androidHomeInGlobalEnv);
if (!sdkRootEnv) {
Logger.log(`Run: ${colors.cyan('npx @nightwatch/mobile-helper android --standalone')} to fix this issue.`);
Logger.log(`(Remove the ${colors.gray('--standalone')} flag from the above command if using the tool for testing.)\n`);

return false;
}
this.sdkRoot = sdkRootEnv;

return this.executeDotCommand();
}

loadEnvFromDotEnv(): void {
this.androidHomeInGlobalEnv = 'ANDROID_HOME' in process.env;
dotenv.config({path: path.join(this.rootDir, '.env')});
}

buildCommand(): string {
const binaryName = this.dotcmd.split('.')[1] as SdkBinary;
const binaryLocation = getBinaryLocation(this.sdkRoot, this.platform, binaryName, true);

let cmd: string;
if (binaryLocation === 'PATH') {
const binaryFullName = getBinaryNameForOS(this.platform, binaryName);
cmd = `${binaryFullName}`;
} else {
const binaryFullName = path.basename(binaryLocation);
const binaryDirPath = path.dirname(binaryLocation);
cmd = path.join(binaryDirPath, binaryFullName);
}

return cmd;
}

executeDotCommand(): boolean {
const cmd = this.buildCommand();
const result = spawnSync(cmd, this.args, {stdio: 'inherit'});

if (result.error) {
console.error(result.error);

return false;
}

return result.status === 0;
}
}

28 changes: 28 additions & 0 deletions src/commands/android/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import colors from 'ansi-colors';

import {AndroidSetup} from './androidSetup';
import {Options} from './interfaces';
import {AndroidSubcommand} from './subcommands';
import {getSubcommandHelp} from './utils/common';
import {AndroidDotCommand} from './dotcommands';

export function handleAndroidCommand(args: string[], options: Options, argv: string[]): void {
if (args[0].includes('.')) {
// Here args[0] represents the android dot command
const androidDotCommand = new AndroidDotCommand(args[0], argv, process.cwd());
androidDotCommand.run();
} else if (args.length === 1) {
const androidSetup = new AndroidSetup(options);
androidSetup.run();
} else if (args.length === 2) {
// Here args[1] represents the android subcommand.
const androidSubcommand = new AndroidSubcommand(args[1], options);
androidSubcommand.run();
} else {
// android command doesn't accept more than one argument.
console.log(`${colors.red(`Too many arguments passed for 'android' command: ${args.slice(1).join(', ')}`)}\n`);

console.log(getSubcommandHelp());
}
}

10 changes: 0 additions & 10 deletions src/commands/android/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,6 @@ export interface Options {
[key: string]: string | string[] | boolean;
}

export interface AvailableSubcommands {
[key: string]: {
description: string;
options: {
name: string;
description: string;
}[];
}
}

export type Platform = 'windows' | 'linux' | 'mac';

export interface OtherInfo {
Expand Down
149 changes: 149 additions & 0 deletions src/commands/android/subcommands/common.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import colors from 'ansi-colors';

import Logger from '../../../logger';
import {symbols} from '../../../utils';
import {AVAILABLE_SUBCOMMANDS} from '../constants';
import {Platform, Options, SdkBinary} from '../interfaces';
import ADB from '../utils/appium-adb';
import {execBinarySync} from '../utils/sdk';
import {CliConfig, SubcommandOptionsVerificationResult} from './interfaces';
import {showHelp} from './help';

const deviceStateWithColor = (state: string) => {
switch (state) {
Expand Down Expand Up @@ -66,3 +72,146 @@ export async function showConnectedEmulators() {
return false;
}
}

export async function getInstalledSystemImages(sdkmanagerLocation: string, platform: Platform): Promise<{
result: boolean,
systemImages: string[]
}> {
const stdout = execBinarySync(sdkmanagerLocation, 'sdkmanager', platform, '--list');
if (!stdout) {
Logger.log(`\n${colors.red('Failed to fetch system images!')} Please try again.`);

return {
result: false,
systemImages: []
};
}
const lines = stdout.split('\n');
const installedImages: string[] = [];

for (const line of lines) {
if (line.includes('Available Packages:')) {
break;
}
if (line.includes('system-images')) {
installedImages.push(line.split('|')[0].trim());
}
}

return {
result: true,
systemImages: installedImages
};
}

export function showMissingBinaryHelp(binaryName: SdkBinary) {
Logger.log(` ${colors.red(symbols().fail)} ${colors.cyan(binaryName)} binary not found.\n`);
Logger.log(`Run: ${colors.cyan('npx @nightwatch/mobile-helper android --standalone')} to setup missing requirements.`);
Logger.log(`(Remove the ${colors.gray('--standalone')} flag from the above command if setting up for testing.)\n`);
}

export function verifyOptions(subcommand: string, options: Options): SubcommandOptionsVerificationResult | false {
const optionsPassed = Object.keys(options).filter(option => options[option] !== false);

const allowedFlags = AVAILABLE_SUBCOMMANDS[subcommand].flags;
const allowedFlagNames = allowedFlags.map(flag => flag.name);

// Divide the optionsPassed array in two arrays: flagsPassed and configsPassed.
// flagsPassed contains the flags that are available for the subcommand.
// configsPassed contains the config options with string or boolean values corresponding to the flag.
const flagsPassed = optionsPassed.filter(option => allowedFlagNames.includes(option));
const configsPassed = optionsPassed.filter(option => !allowedFlagNames.includes(option));

// CHECK THE VALIDITY OF FLAG(s) PASSED

if (flagsPassed.length > 1) {
// A subcommand can only take one flag at a time.
Logger.log(`${colors.red(`Too many flags passed for '${subcommand}' subcommand:`)} ${flagsPassed.join(', ')} ${colors.gray('(only one expected)')}`);
showHelp(subcommand);

return false;
}

if (allowedFlags.length && flagsPassed.length === 0) {
// If the subcommand expects a flag but it is not passed:
// - if instead some other options are passed, throw error (we don't know if the options passed are configs and for which flag).
// - if no other options are passed, then we can prompt them for the flag and related configs.
if (configsPassed.length > 0) {
Logger.log(`${colors.red(`Unknown flag(s) passed for '${subcommand}' subcommand:`)} ${configsPassed.join(', ')}`);
showHelp(subcommand);

return false;
}

return {
subcommandFlag: '',
configs: []
};
}

// CHECK THE VALIDITY OF CONFIGS PASSED

const subcommandFlag = flagsPassed[0] || ''; // '' if no flag is allowed for the subcommand.

if (configsPassed.length === 0) {
// If no configs are passed, then we simply return and continue with the default subcommand flow.
return {
subcommandFlag,
configs: []
};
}

let allowedConfigs: CliConfig[] = [];
let configsFor = '';
if (!allowedFlags.length) {
allowedConfigs = AVAILABLE_SUBCOMMANDS[subcommand].cliConfigs || [];
configsFor = ` for '${subcommand}' subcommand`;
} else {
allowedConfigs = allowedFlags.find(flag => flag.name === subcommandFlag)?.cliConfigs || [];
configsFor = ` for '--${subcommandFlag}' flag`;
}

if (allowedConfigs.length) {
// Check if the passed configs are valid.
const configNames: string[] = allowedConfigs.map(config => config.name);

const configAliases: string[] = [];
allowedConfigs.forEach(config => configAliases.push(...config.alias));
configNames.push(...configAliases);

const unknownConfigs = configsPassed.filter(option => !configNames.includes(option));
if (unknownConfigs.length) {
Logger.log(`${colors.red(`Unknown config(s) passed${configsFor}:`)} ${unknownConfigs.join(', ')}`);
showHelp(subcommand);

return false;
}

// set main config in `options` if config aliases are passed.
const aliasToMainConfig: {[key: string]: string} = {};
allowedConfigs.forEach(config => {
config.alias.forEach(alias => {
aliasToMainConfig[alias] = config.name;
});
});

configsPassed.forEach((configName) => {
if (aliasToMainConfig[configName]) {
// `configName` is an alias
const mainConfig = aliasToMainConfig[configName];
options[mainConfig] = options[configName];
}
});
} else {
// if no configs are allowed for the flag but still some options are passed, then throw error.
Logger.log(`${colors.red(`Unknown config(s) passed${configsFor}:`)} ${configsPassed.join(', ')} ${colors.gray('(none expected)')}`);
showHelp(subcommand);

return false;
}

return {
subcommandFlag,
configs: configsPassed
};
}
14 changes: 12 additions & 2 deletions src/commands/android/subcommands/connect/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import {Options, Platform} from '../../interfaces';
import {showConnectedRealDevices} from '../common';
import {verifyOptions, showConnectedRealDevices} from '../common';
import {connectWirelessAdb} from './wireless';

export async function connect(options: Options, sdkRoot: string, platform: Platform): Promise<boolean> {
if (options.wireless) {
const verifyResult = verifyOptions('connect', options);
if (!verifyResult) {
return false;
}

const subcommandFlag = verifyResult.subcommandFlag;
if (subcommandFlag === '') {
// flag not passed by the user -- prompt user for the flag
}

if (subcommandFlag === 'wireless') {
await showConnectedRealDevices();

return await connectWirelessAdb(sdkRoot, platform);
Expand Down
Loading

0 comments on commit 553fa4b

Please sign in to comment.