From 61711efd69e8eaafe3affc938ac91362f59c54ce Mon Sep 17 00:00:00 2001 From: Priyansh Prajapati <88396544+itsspriyansh@users.noreply.github.com> Date: Tue, 20 Aug 2024 00:26:37 +0530 Subject: [PATCH 01/10] Add support for Android dotcommands. (#47) --- src/commands/android/dotcommands.ts | 96 +++++++++++++++++++++++++++++ src/commands/android/index.ts | 9 ++- src/constants.ts | 1 + src/index.ts | 4 +- 4 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 src/commands/android/dotcommands.ts diff --git a/src/commands/android/dotcommands.ts b/src/commands/android/dotcommands.ts new file mode 100644 index 0000000..e9378f0 --- /dev/null +++ b/src/commands/android/dotcommands.ts @@ -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 { + 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 [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; + } +} + diff --git a/src/commands/android/index.ts b/src/commands/android/index.ts index ed7ef0a..f224c3b 100644 --- a/src/commands/android/index.ts +++ b/src/commands/android/index.ts @@ -4,9 +4,14 @@ 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): void { - if (args.length === 1) { +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) { diff --git a/src/constants.ts b/src/constants.ts index 89bb1b4..86c6ca1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1 +1,2 @@ export const AVAILABLE_COMMANDS = ['android', 'ios']; +export const ANDROID_DOTCOMMANDS = ['android.emulator', 'android.avdmanager', 'android.sdkmanager', 'android.adb']; diff --git a/src/index.ts b/src/index.ts index 24b9c4d..b99e756 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,8 +25,8 @@ export const run = () => { console.log(`${colors.red('No command passed.')}\n`); } showHelp(); - } else if (args[0] === 'android') { - handleAndroidCommand(args, options); + } else if (args[0].split('.')[0] === 'android') { + handleAndroidCommand(args, options, argv); } else if (args[0] === 'ios') { if (args.length > 1) { // ios command does not accept subcommands. From ea0ffd0ecae2f09d41af368183e13e679a529ccf Mon Sep 17 00:00:00 2001 From: Priyansh Prajapati <88396544+itsspriyansh@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:56:39 +0530 Subject: [PATCH 02/10] feat: Add flow for creating AVDs (#58) Co-authored-by: Priyansh Garg --- src/commands/android/subcommands/common.ts | 34 +++- .../android/subcommands/install/avd.ts | 145 ++++++++++++++++++ .../android/subcommands/install/index.ts | 3 + 3 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 src/commands/android/subcommands/install/avd.ts diff --git a/src/commands/android/subcommands/common.ts b/src/commands/android/subcommands/common.ts index b4f26e3..a0c0b50 100644 --- a/src/commands/android/subcommands/common.ts +++ b/src/commands/android/subcommands/common.ts @@ -3,8 +3,9 @@ import colors from 'ansi-colors'; import Logger from '../../../logger'; import {symbols} from '../../../utils'; import {AVAILABLE_SUBCOMMANDS} from '../constants'; -import {Options, SdkBinary} from '../interfaces'; +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'; @@ -72,6 +73,37 @@ export async function showConnectedEmulators() { } } +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.`); diff --git a/src/commands/android/subcommands/install/avd.ts b/src/commands/android/subcommands/install/avd.ts new file mode 100644 index 0000000..a3ab64b --- /dev/null +++ b/src/commands/android/subcommands/install/avd.ts @@ -0,0 +1,145 @@ +import colors from 'ansi-colors'; +import inquirer from 'inquirer'; + +import Logger from '../../../../logger'; +import {Platform} from '../../interfaces'; +import {getBinaryLocation} from '../../utils/common'; +import {execBinaryAsync, execBinarySync} from '../../utils/sdk'; +import {getInstalledSystemImages, showMissingBinaryHelp} from '../common'; + +type DeviceType = 'Nexus' | 'Pixel' | 'Wear OS' | 'Android TV' | 'Desktop' | 'Others'; + +const deviceTypesToGrepCommand: Record = { + 'Nexus': 'Nexus', + 'Pixel': 'pixel', + 'Wear OS': 'wear', + 'Android TV': 'tv', + 'Desktop': 'desktop', + 'Others': '-Ev "wear|Nexus|pixel|tv|desktop"' +}; + +export async function createAvd(sdkRoot: string, platform: Platform): Promise { + try { + const avdmanagerLocation = getBinaryLocation(sdkRoot, platform, 'avdmanager', true); + if (!avdmanagerLocation) { + showMissingBinaryHelp('avdmanager'); + + return false; + } + + const sdkmanagerLocation = getBinaryLocation(sdkRoot, platform, 'sdkmanager', true); + if (!sdkmanagerLocation) { + showMissingBinaryHelp('sdkmanager'); + + return false; + } + + const avdNameAnswer = await inquirer.prompt({ + type: 'input', + name: 'avdName', + message: 'Enter a name for the AVD:' + }); + const avdName = avdNameAnswer.avdName || 'my_avd'; + + const installedSystemImages = await getInstalledSystemImages(sdkmanagerLocation, platform); + if (!installedSystemImages.result) { + return false; + } + if (!installedSystemImages.systemImages.length) { + Logger.log(colors.red('\nNo installed system images were found!')); + Logger.log(`Run: ${colors.cyan('npx @nightwatch/mobile-helper android install --system-image')} to install a new system image.`); + + return false; + } + + const systemImageAnswer = await inquirer.prompt({ + type: 'list', + name: 'systemImage', + message: 'Select the system image to use for AVD:', + choices: installedSystemImages.systemImages + }); + const systemImage = systemImageAnswer.systemImage; + + const deviceTypeAnswer = await inquirer.prompt({ + type: 'list', + name: 'deviceType', + message: 'Select the device type for AVD:', + choices: Object.keys(deviceTypesToGrepCommand) + }); + const deviceType = deviceTypeAnswer.deviceType; + + let cmd = `list devices -c | grep ${deviceTypesToGrepCommand[deviceType as DeviceType]}`; + const availableDeviceProfiles = execBinarySync(avdmanagerLocation, 'avdmanager', platform, cmd); + + if (!availableDeviceProfiles) { + Logger.log(`${colors.red(`No potential device profile found for device type ${deviceType}.`)} Please try again.`); + + return false; + } + const availableDeviceProfilesList = availableDeviceProfiles.split('\n').filter(deviceProfile => deviceProfile !== ''); + const deviceAnswer = await inquirer.prompt({ + type: 'list', + name: 'deviceProfile', + message: 'Select the device profile for AVD:', + choices: availableDeviceProfilesList + }); + const deviceProfile = deviceAnswer.deviceProfile; + + Logger.log(); + Logger.log('Creating AVD...\n'); + + cmd = `create avd -n '${avdName}' -k '${systemImage}' -d '${deviceProfile}'`; + let createAVDStatus = false; + + try { + createAVDStatus = await executeCreateAvdCommand(cmd, avdmanagerLocation, platform, avdName); + } catch (err) { + if (typeof err === 'string' && err.includes('already exists')) { + // AVD with the same name already exists. Ask user if they want to overwrite it. + Logger.log(`\n${colors.yellow('AVD with the same name already exists!')}\n`); + const overwriteAnswer = await inquirer.prompt({ + type: 'confirm', + name: 'overwrite', + message: 'Overwrite the existing AVD?' + }); + Logger.log(); + + if (overwriteAnswer.overwrite) { + cmd += ' --force'; + createAVDStatus = await executeCreateAvdCommand(cmd, avdmanagerLocation, platform, avdName); + } + } else { + handleError(err); + } + } + + return createAVDStatus; + } catch (err) { + handleError(err); + + return false; + } +} + +async function executeCreateAvdCommand(cmd: string, avdmanagerLocation: string, platform: Platform, avdName: string): Promise { + const output = await execBinaryAsync(avdmanagerLocation, 'avdmanager', platform, cmd); + + if (output?.includes('100% Fetch remote repository')) { + Logger.log(colors.green('AVD created successfully!\n')); + Logger.log(`Run ${colors.cyan(`npx @nightwatch/mobile-helper android connect --emulator --avd ${avdName}`)} to launch the AVD.\n`); + + return true; + } + + Logger.log(colors.red('Something went wrong while creating AVD!')); + Logger.log(`Please run ${colors.cyan(`npx @nightwatch/mobile-helper android connect --emulator --avd ${avdName}`)} to verify AVD creation.`); + Logger.log('If AVD does not launch, please try creating the AVD again.\n'); + + return false; +} + +function handleError(err: any) { + Logger.log(colors.red('\nError occurred while creating AVD!')); + console.error(err); +} + diff --git a/src/commands/android/subcommands/install/index.ts b/src/commands/android/subcommands/install/index.ts index cf429da..8c3b0c1 100644 --- a/src/commands/android/subcommands/install/index.ts +++ b/src/commands/android/subcommands/install/index.ts @@ -1,9 +1,12 @@ import {Options, Platform} from '../../interfaces'; import {installApp} from './app'; +import {createAvd} from './avd'; export async function install(options: Options, sdkRoot: string, platform: Platform): Promise { if (options.app) { return await installApp(options, sdkRoot, platform); + } else if (options.avd) { + return await createAvd(sdkRoot, platform); } return false; From e84c677e0e385e652e2f4081259bcdb750ec34db Mon Sep 17 00:00:00 2001 From: Priyansh Prajapati <88396544+itsspriyansh@users.noreply.github.com> Date: Thu, 22 Aug 2024 00:54:25 +0530 Subject: [PATCH 03/10] Show help for invalid subcommand. (#48) Co-authored-by: Priyansh Garg --- src/commands/android/subcommands/index.ts | 15 +++++++++++++-- src/commands/android/utils/common.ts | 6 +++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/commands/android/subcommands/index.ts b/src/commands/android/subcommands/index.ts index a2ff1ee..cdcc05d 100644 --- a/src/commands/android/subcommands/index.ts +++ b/src/commands/android/subcommands/index.ts @@ -4,10 +4,11 @@ import path from 'path'; import Logger from '../../../logger'; import {getPlatformName} from '../../../utils'; +import {AVAILABLE_SUBCOMMANDS} from '../constants'; import {Options, Platform} from '../interfaces'; -import {checkJavaInstallation, getSdkRootFromEnv} from '../utils/common'; -import {showHelp} from './help'; +import {checkJavaInstallation, getSdkRootFromEnv, getSubcommandHelp} from '../utils/common'; import {connect} from './connect'; +import {showHelp} from './help'; import {install} from './install'; export class AndroidSubcommand { @@ -28,11 +29,21 @@ export class AndroidSubcommand { } async run(): Promise { + if (!Object.keys(AVAILABLE_SUBCOMMANDS).includes(this.subcommand)) { + Logger.log(`${colors.red(`unknown subcommand passed: ${this.subcommand}`)}\n`); + Logger.log(getSubcommandHelp()); + Logger.log(`For individual subcommand help, run: ${colors.cyan('npx @nightwatch/mobile-helper android SUBCOMMAND --help')}`); + Logger.log(`For complete Android help, run: ${colors.cyan('npx @nightwatch/mobile-helper android --help')}\n`); + + return false; + } + if (this.options.help) { showHelp(this.subcommand); return true; } + this.loadEnvFromDotEnv(); const javaInstalled = checkJavaInstallation(this.rootDir); diff --git a/src/commands/android/utils/common.ts b/src/commands/android/utils/common.ts index ab7ea2a..0ae2b28 100644 --- a/src/commands/android/utils/common.ts +++ b/src/commands/android/utils/common.ts @@ -216,9 +216,9 @@ export const checkJavaInstallation = (cwd: string): boolean => { export const getSubcommandHelp = (): string => { let output = ''; - output += `Usage: ${colors.cyan('npx @nightwatch/mobile-helper android [subcmd] [subcmd-options]')}\n`; - output += ' The following subcommands are used for different operations on Android SDK:\n\n'; - output += `${colors.yellow('Subcommands and Subcommand-Options:')}\n`; + output += `Usage: ${colors.cyan('npx @nightwatch/mobile-helper android SUBCOMMAND [flag] [configs]')}\n`; + output += ' Perform common Android SDK operations using subcommands.\n\n'; + output += `${colors.yellow('Subcommands (with available flags and configs):')}\n`; Object.keys(AVAILABLE_SUBCOMMANDS).forEach(subcommand => { const subcmd = AVAILABLE_SUBCOMMANDS[subcommand]; From 1c5410ef2b62cde5fb9132e255a00cf38ad00229 Mon Sep 17 00:00:00 2001 From: Priyansh Prajapati <88396544+itsspriyansh@users.noreply.github.com> Date: Thu, 22 Aug 2024 01:11:42 +0530 Subject: [PATCH 04/10] Add default flow for the `install` subcommand (#67) --- src/commands/android/constants.ts | 27 ++++++++++++++ .../android/subcommands/install/app.ts | 2 +- .../android/subcommands/install/index.ts | 35 +++++++++++++++++-- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/commands/android/constants.ts b/src/commands/android/constants.ts index b086e61..4c30706 100644 --- a/src/commands/android/constants.ts +++ b/src/commands/android/constants.ts @@ -41,6 +41,33 @@ export const AVAILABLE_SUBCOMMANDS: AvailableSubcommands = { description: 'Connect a real device wirelessly' } ] + }, + 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' + } + ] + } + ] } }; diff --git a/src/commands/android/subcommands/install/app.ts b/src/commands/android/subcommands/install/app.ts index 8231980..607d90d 100644 --- a/src/commands/android/subcommands/install/app.ts +++ b/src/commands/android/subcommands/install/app.ts @@ -95,7 +95,7 @@ const handleError = (consoleOutput: any) => { let errorMessage = consoleOutput; if (consoleOutput.includes('INSTALL_FAILED_ALREADY_EXISTS')) { errorMessage = 'APK with the same package name already exists on the device.\n'; - errorMessage += colors.reset(`\nPlease uninstall the app first from the device and then install again.\n`); + errorMessage += colors.reset('\nPlease uninstall the app first from the device and then install again.\n'); errorMessage += colors.reset(`To uninstall, use: ${colors.cyan('npx @nightwatch/mobile-helper android uninstall --app')}\n`); } else if (consoleOutput.includes('INSTALL_FAILED_OLDER_SDK')) { errorMessage = 'Target installation location (AVD/Real device) has older SDK version than the minimum requirement of the APK.\n'; diff --git a/src/commands/android/subcommands/install/index.ts b/src/commands/android/subcommands/install/index.ts index 8c3b0c1..6387629 100644 --- a/src/commands/android/subcommands/install/index.ts +++ b/src/commands/android/subcommands/install/index.ts @@ -1,14 +1,45 @@ +import inquirer from 'inquirer'; + +import Logger from '../../../../logger'; import {Options, Platform} from '../../interfaces'; +import {verifyOptions} from '../common'; import {installApp} from './app'; import {createAvd} from './avd'; export async function install(options: Options, sdkRoot: string, platform: Platform): Promise { - if (options.app) { + const optionsVerified = verifyOptions('install', options); + if (!optionsVerified) { + return false; + } + + let subcommandFlag = optionsVerified.subcommandFlag; + if (subcommandFlag === '') { + subcommandFlag = await promptForFlag(); + } + + if (subcommandFlag === 'app') { return await installApp(options, sdkRoot, platform); - } else if (options.avd) { + } else if (subcommandFlag === 'avd') { return await createAvd(sdkRoot, platform); } return false; } +async function promptForFlag(): Promise { + const flagAnswer = await inquirer.prompt({ + type: 'list', + name: 'flag', + message: 'Select what do you want to install:', + choices: ['APK', 'AVD'] + }); + Logger.log(); + + const flag = flagAnswer.flag; + if (flag === 'APK') { + return 'app'; + } + + return 'avd'; +} + From 8234c7cf81f74e3db3e6a2bd34cf5a4da06fd003 Mon Sep 17 00:00:00 2001 From: Priyansh Prajapati <88396544+itsspriyansh@users.noreply.github.com> Date: Thu, 22 Aug 2024 12:18:58 +0530 Subject: [PATCH 05/10] feat: list installed avd and connected devices (#66) --- src/commands/android/constants.ts | 11 +++++ src/commands/android/subcommands/index.ts | 3 ++ src/commands/android/subcommands/list/avd.ts | 38 ++++++++++++++++ .../android/subcommands/list/device.ts | 30 +++++++++++++ .../android/subcommands/list/index.ts | 44 +++++++++++++++++++ 5 files changed, 126 insertions(+) create mode 100644 src/commands/android/subcommands/list/avd.ts create mode 100644 src/commands/android/subcommands/list/device.ts create mode 100644 src/commands/android/subcommands/list/index.ts diff --git a/src/commands/android/constants.ts b/src/commands/android/constants.ts index 4c30706..c504bd7 100644 --- a/src/commands/android/constants.ts +++ b/src/commands/android/constants.ts @@ -42,6 +42,17 @@ export const AVAILABLE_SUBCOMMANDS: AvailableSubcommands = { } ] }, + 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: [ diff --git a/src/commands/android/subcommands/index.ts b/src/commands/android/subcommands/index.ts index cdcc05d..47209b8 100644 --- a/src/commands/android/subcommands/index.ts +++ b/src/commands/android/subcommands/index.ts @@ -10,6 +10,7 @@ import {checkJavaInstallation, getSdkRootFromEnv, getSubcommandHelp} from '../ut import {connect} from './connect'; import {showHelp} from './help'; import {install} from './install'; +import {list} from './list'; export class AndroidSubcommand { sdkRoot: string; @@ -76,6 +77,8 @@ export class AndroidSubcommand { return await connect(this.options, this.sdkRoot, this.platform); } else if (this.subcommand === 'install') { return await install(this.options, this.sdkRoot, this.platform); + } else if (this.subcommand === 'list') { + return await list(this.options, this.sdkRoot, this.platform); } return false; diff --git a/src/commands/android/subcommands/list/avd.ts b/src/commands/android/subcommands/list/avd.ts new file mode 100644 index 0000000..93f3f04 --- /dev/null +++ b/src/commands/android/subcommands/list/avd.ts @@ -0,0 +1,38 @@ +import colors from 'ansi-colors'; + +import Logger from '../../../../logger'; +import {Platform} from '../../interfaces'; +import {getBinaryLocation} from '../../utils/common'; +import {execBinarySync} from '../../utils/sdk'; +import {showMissingBinaryHelp} from '../common'; + +export async function listInstalledAVDs(sdkRoot: string, platform: Platform): Promise { + try { + const avdmanagerLocation = getBinaryLocation(sdkRoot, platform, 'avdmanager', true); + if (!avdmanagerLocation) { + showMissingBinaryHelp('avdmanager'); + + return false; + } + + const installedAVDs = execBinarySync(avdmanagerLocation, 'avd', platform, 'list avd'); + if (!installedAVDs) { + Logger.log(`\n${colors.red('Failed to list installed AVDs!')} Please try again.`); + + return false; + } + + if (installedAVDs.split('\n').length < 3) { + Logger.log(colors.red('No installed AVDs found!')); + } else { + Logger.log(installedAVDs); + } + + return true; + } catch (err) { + Logger.log(colors.red('Error occurred while listing installed AVDs.')); + console.error(err); + + return false; + } +} diff --git a/src/commands/android/subcommands/list/device.ts b/src/commands/android/subcommands/list/device.ts new file mode 100644 index 0000000..35c50b9 --- /dev/null +++ b/src/commands/android/subcommands/list/device.ts @@ -0,0 +1,30 @@ +import colors from 'ansi-colors'; + +import Logger from '../../../../logger'; +import {Platform} from '../../interfaces'; +import ADB from '../../utils/appium-adb'; +import {getBinaryLocation} from '../../utils/common'; +import {showConnectedEmulators, showConnectedRealDevices, showMissingBinaryHelp} from '../common'; + +export async function listConnectedDevices(sdkRoot: string, platform: Platform): Promise { + const adbLocation = getBinaryLocation(sdkRoot, platform, 'adb', true); + if (adbLocation === '') { + showMissingBinaryHelp('adb'); + + return false; + } + + const adb = await ADB.createADB({allowOfflineDevices: true}); + const devices = await adb.getConnectedDevices(); + + if (!devices.length) { + Logger.log(colors.yellow('No connected devices found.\n')); + + return true; + } + + await showConnectedRealDevices(); + await showConnectedEmulators(); + + return true; +} diff --git a/src/commands/android/subcommands/list/index.ts b/src/commands/android/subcommands/list/index.ts new file mode 100644 index 0000000..0bf7cd1 --- /dev/null +++ b/src/commands/android/subcommands/list/index.ts @@ -0,0 +1,44 @@ +import inquirer from 'inquirer'; + +import Logger from '../../../../logger'; +import {Options, Platform} from '../../interfaces'; +import {verifyOptions} from '../common'; +import {listInstalledAVDs} from './avd'; +import {listConnectedDevices} from './device'; + +export async function list(options: Options, sdkRoot: string, platform: Platform): Promise { + const optionsVerified = verifyOptions('list', options); + if (!optionsVerified) { + return false; + } + + let subcommandFlag = optionsVerified.subcommandFlag; + if (subcommandFlag === '') { + subcommandFlag = await promptForFlag(); + } + + if (subcommandFlag === 'avd') { + return await listInstalledAVDs(sdkRoot, platform); + } else if (subcommandFlag === 'device') { + return await listConnectedDevices(sdkRoot, platform); + } + + return false; +} + +async function promptForFlag(): Promise { + const flagAnswer = await inquirer.prompt({ + type: 'list', + name: 'flag', + message: 'Select what do you want to list:', + choices: ['Connected devices', 'Installed AVDs'] + }); + Logger.log(); + + const flag = flagAnswer.flag; + if (flag === 'Connected devices') { + return 'device'; + } + + return 'avd'; +} From d7d9a8158e9073cc79cc15a83ce35ae50c993d66 Mon Sep 17 00:00:00 2001 From: Priyansh Prajapati <88396544+itsspriyansh@users.noreply.github.com> Date: Sat, 24 Aug 2024 01:49:29 +0530 Subject: [PATCH 06/10] feat: uninstall avd (#60) Co-authored-by: Priyansh Garg --- src/commands/android/constants.ts | 6 ++ src/commands/android/subcommands/index.ts | 7 ++- .../android/subcommands/uninstall/avd.ts | 63 +++++++++++++++++++ .../android/subcommands/uninstall/index.ts | 11 ++++ 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 src/commands/android/subcommands/uninstall/avd.ts create mode 100644 src/commands/android/subcommands/uninstall/index.ts diff --git a/src/commands/android/constants.ts b/src/commands/android/constants.ts index c504bd7..55d923e 100644 --- a/src/commands/android/constants.ts +++ b/src/commands/android/constants.ts @@ -79,6 +79,12 @@ export const AVAILABLE_SUBCOMMANDS: AvailableSubcommands = { ] } ] + }, + uninstall: { + description: 'todo item', + flags: [ + {name: 'avd', description: 'todo item'}, + ] } }; diff --git a/src/commands/android/subcommands/index.ts b/src/commands/android/subcommands/index.ts index 47209b8..b16e20f 100644 --- a/src/commands/android/subcommands/index.ts +++ b/src/commands/android/subcommands/index.ts @@ -11,6 +11,7 @@ import {connect} from './connect'; import {showHelp} from './help'; import {install} from './install'; import {list} from './list'; +import {uninstall} from './uninstall'; export class AndroidSubcommand { sdkRoot: string; @@ -61,9 +62,7 @@ export class AndroidSubcommand { } this.sdkRoot = sdkRootEnv; - this.executeSubcommand(); - - return false; + return await this.executeSubcommand(); } loadEnvFromDotEnv(): void { @@ -79,6 +78,8 @@ export class AndroidSubcommand { return await install(this.options, this.sdkRoot, this.platform); } else if (this.subcommand === 'list') { return await list(this.options, this.sdkRoot, this.platform); + } else if (this.subcommand === 'uninstall') { + return await uninstall(this.options, this.sdkRoot, this.platform); } return false; diff --git a/src/commands/android/subcommands/uninstall/avd.ts b/src/commands/android/subcommands/uninstall/avd.ts new file mode 100644 index 0000000..7368611 --- /dev/null +++ b/src/commands/android/subcommands/uninstall/avd.ts @@ -0,0 +1,63 @@ +import colors from 'ansi-colors'; +import inquirer from 'inquirer'; + +import Logger from '../../../../logger'; +import {Platform} from '../../interfaces'; +import {getBinaryLocation} from '../../utils/common'; +import {execBinarySync, execBinaryAsync} from '../../utils/sdk'; +import {showMissingBinaryHelp} from '../common'; + +export async function deleteAvd(sdkRoot: string, platform: Platform): Promise { + try { + const avdmanagerLocation = getBinaryLocation(sdkRoot, platform, 'avdmanager', true); + if (!avdmanagerLocation) { + showMissingBinaryHelp('avdmanager'); + + return false; + } + + const installedAvds = execBinarySync(avdmanagerLocation, 'avdmanager', platform, 'list avd -c'); + if (installedAvds === null) { + Logger.log(`${colors.red('\nFailed to fetch installed AVDs.')} Please try again.\n`); + + return false; + } else if (installedAvds === '') { + Logger.log(`${colors.yellow('No installed AVD found.')}\n`); + Logger.log('To see the list of installed AVDs, run the following command:'); + Logger.log(colors.cyan(' npx @nightwatch/mobile-helper android list --avd\n')); + + return false; + } + + const avdAnswer = await inquirer.prompt({ + type: 'list', + name: 'avdName', + message: 'Select the AVD to delete:', + choices: installedAvds.split('\n').filter(avd => avd !== '') + }); + const avdName = avdAnswer.avdName; + + Logger.log(); + Logger.log(`Deleting ${colors.cyan(avdName)}...\n`); + + const deleteStatus = await execBinaryAsync(avdmanagerLocation, 'avdmanager', platform, `delete avd --name '${avdName}'`); + + if (deleteStatus?.includes('deleted')) { + Logger.log(colors.green('AVD deleted successfully!\n')); + + return true; + } + + Logger.log(colors.red('Something went wrong while deleting AVD.')); + Logger.log('Command output:', deleteStatus); + Logger.log(`To verify if the AVD was deleted, run: ${colors.cyan('npx @nightwatch/mobile-helper android list --avd')}`); + Logger.log('If the AVD is still present, try deleting it again.\n'); + + return false; + } catch (error) { + Logger.log(colors.red('\nError occurred while deleting AVD.')); + console.error(error); + + return false; + } +} diff --git a/src/commands/android/subcommands/uninstall/index.ts b/src/commands/android/subcommands/uninstall/index.ts new file mode 100644 index 0000000..0947987 --- /dev/null +++ b/src/commands/android/subcommands/uninstall/index.ts @@ -0,0 +1,11 @@ +import {Options, Platform} from '../../interfaces'; +import {deleteAvd} from './avd'; + +export async function uninstall(options: Options, sdkRoot: string, platform: Platform): Promise { + if (options.avd) { + return await deleteAvd(sdkRoot, platform); + } + + return false; +} + From f0d4d02b09650261f2f327c191c3f28f4af09ec2 Mon Sep 17 00:00:00 2001 From: Priyansh Prajapati <88396544+itsspriyansh@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:17:05 +0530 Subject: [PATCH 07/10] tests: Add unit tests for `verifyOptions` (#65) Co-authored-by: Priyansh Garg --- .../android/subcommands/testCommon.js | 497 ++++++++++++++++++ 1 file changed, 497 insertions(+) create mode 100644 tests/unit_tests/commands/android/subcommands/testCommon.js diff --git a/tests/unit_tests/commands/android/subcommands/testCommon.js b/tests/unit_tests/commands/android/subcommands/testCommon.js new file mode 100644 index 0000000..140e671 --- /dev/null +++ b/tests/unit_tests/commands/android/subcommands/testCommon.js @@ -0,0 +1,497 @@ +const assert = require('assert'); +const mockery = require('mockery'); + +describe('test verifyOptions', function() { + beforeEach(() => { + mockery.enable({useCleanCache: true, warnOnReplace: false, warnOnUnregistered: false}); + + mockery.registerMock('./adb', {}); + }); + + afterEach(() => { + mockery.deregisterAll(); + mockery.resetCache(); + mockery.disable(); + }); + + // --- (allowedFlags.length > 0) starts here --- + it('shows error if more than one flags passed', () => { + const consoleOutput = []; + mockery.registerMock( + '../../../logger', + class { + static log(...msgs) { + consoleOutput.push(...msgs); + } + } + ); + + const colorFn = (arg) => arg; + mockery.registerMock('ansi-colors', { + green: colorFn, + yellow: colorFn, + magenta: colorFn, + cyan: colorFn, + red: colorFn, + gray: colorFn, + grey: colorFn + }); + + mockery.registerMock('../constants', { + AVAILABLE_SUBCOMMANDS: { + subcmd1: { + flags: [{ + name: 'flag1' + }, + { + name: 'flag2' + }] + } + } + }); + + // without configs + const {verifyOptions} = require('../../../../../src/commands/android/subcommands/common'); + const result1 = verifyOptions('subcmd1', {flag1: true, flag2: true}); + const output1 = consoleOutput.toString(); + assert.strictEqual(output1.includes('Too many flags passed for \'subcmd1\' subcommand: flag1, flag2 (only one expected)'), true); + assert.deepStrictEqual(result1, false); + + // with configs other than flag + consoleOutput.length = 0; + const result2 = verifyOptions('subcmd1', {flag1: true, flag2: true, random: true}); + const output2 = consoleOutput.toString(); + assert.strictEqual(output2.includes('Too many flags passed for \'subcmd1\' subcommand: flag1, flag2 (only one expected)'), true); + assert.deepStrictEqual(result2, false); + }); + + it('shows error for unknown flags', () => { + const consoleOutput = []; + mockery.registerMock( + '../../../logger', + class { + static log(...msgs) { + consoleOutput.push(...msgs); + } + } + ); + + const colorFn = (arg) => arg; + mockery.registerMock('ansi-colors', { + green: colorFn, + yellow: colorFn, + magenta: colorFn, + cyan: colorFn, + red: colorFn, + gray: colorFn, + grey: colorFn + }); + + mockery.registerMock('../constants', { + AVAILABLE_SUBCOMMANDS: { + subcmd1: { + flags: [{ + name: 'flag1' + }] + } + } + }); + + const {verifyOptions} = require('../../../../../src/commands/android/subcommands/common'); + const result = verifyOptions('subcmd1', {random: true}); + const output = consoleOutput.toString(); + assert.strictEqual(output.includes('Unknown flag(s) passed for \'subcmd1\' subcommand: random'), true); + assert.deepStrictEqual(result, false); + + consoleOutput.length = 0; + const result2 = verifyOptions('subcmd1', {random: 'something'}); + const output2 = consoleOutput.toString(); + assert.strictEqual(output2.includes('Unknown flag(s) passed for \'subcmd1\' subcommand: random'), true); + assert.deepStrictEqual(result2, false); + }); + + it('works and does not log anything for no flag and no config passed', () => { + const consoleOutput = []; + mockery.registerMock( + '../../../logger', + class { + static log(...msgs) { + consoleOutput.push(...msgs); + } + } + ); + + const colorFn = (arg) => arg; + mockery.registerMock('ansi-colors', { + green: colorFn, + yellow: colorFn, + magenta: colorFn, + cyan: colorFn, + red: colorFn, + gray: colorFn, + grey: colorFn + }); + + mockery.registerMock('../constants', { + AVAILABLE_SUBCOMMANDS: { + subcmd1: { + flags: [{ + name: 'flag1', + cliConfigs: [{ + name: 'config1', + alias: ['c1'] + }] + }] + }, + subcmd2: { + flags: [], + cliConfigs: [{ + name: 'config2', + alias: ['c2'] + }] + }, + subcmd3: { + flags: [{ + name: 'flag1' + }], + cliConfigs: [] + } + } + }); + + const {verifyOptions} = require('../../../../../src/commands/android/subcommands/common'); + const result1 = verifyOptions('subcmd1', {}); + assert.strictEqual(consoleOutput.length, 0); + assert.deepStrictEqual(result1, {subcommandFlag: '', configs: []}); + + consoleOutput.length = 0; + const result2 = verifyOptions('subcmd2', {}); + assert.strictEqual(consoleOutput.length, 0); + assert.deepStrictEqual(result2, {subcommandFlag: '', configs: []}); + + consoleOutput.length = 0; + const result3 = verifyOptions('subcmd3', {}); + assert.strictEqual(consoleOutput.length, 0); + assert.deepStrictEqual(result3, {subcommandFlag: '', configs: []}); + }); + + it('works for correct flag (without configs)', () => { + const consoleOutput = []; + mockery.registerMock( + '../../../logger', + class { + static log(...msgs) { + consoleOutput.push(...msgs); + } + } + ); + + const colorFn = (arg) => arg; + mockery.registerMock('ansi-colors', { + green: colorFn, + yellow: colorFn, + magenta: colorFn, + cyan: colorFn, + red: colorFn, + gray: colorFn, + grey: colorFn + }); + + mockery.registerMock('../constants', { + AVAILABLE_SUBCOMMANDS: { + subcmd1: { + flags: [{ + name: 'flag1' + }, + { + name: 'flag2' + }] + } + } + }); + + const {verifyOptions} = require('../../../../../src/commands/android/subcommands/common'); + const result1 = verifyOptions('subcmd1', {flag1: true}); + const output1 = consoleOutput.toString(); + assert.strictEqual(output1.includes('Unknown flag(s) passed'), false); + assert.deepStrictEqual(result1, {subcommandFlag: 'flag1', configs: []}); + + consoleOutput.length = 0; + const result2 = verifyOptions('subcmd1', {flag2: true}); + const output2 = consoleOutput.toString(); + assert.strictEqual(output2.includes('Unknown flag(s) passed'), false); + assert.deepStrictEqual(result2, {subcommandFlag: 'flag2', configs: []}); + }); + + it('shows error for correct flag but unknown configs (allowedConfigs = 0)', () => { + const consoleOutput = []; + mockery.registerMock( + '../../../logger', + class { + static log(...msgs) { + consoleOutput.push(...msgs); + } + } + ); + + const colorFn = (arg) => arg; + mockery.registerMock('ansi-colors', { + green: colorFn, + yellow: colorFn, + magenta: colorFn, + cyan: colorFn, + red: colorFn, + gray: colorFn, + grey: colorFn + }); + + mockery.registerMock('../constants', { + AVAILABLE_SUBCOMMANDS: { + subcmd1: { + flags: [{ + name: 'flag1', + cliConfigs: [] + }] + } + } + }); + + const {verifyOptions} = require('../../../../../src/commands/android/subcommands/common'); + const result = verifyOptions('subcmd1', {flag1: true, random: true}); + const output = consoleOutput.toString(); + assert.strictEqual(output.includes('Unknown config(s) passed for \'--flag1\' flag: random (none expected)'), true); + assert.deepStrictEqual(result, false); + }); + + it('shows error for correct flag but unknown configs (allowedConfigs > 0)', () => { + const consoleOutput = []; + mockery.registerMock( + '../../../logger', + class { + static log(...msgs) { + consoleOutput.push(...msgs); + } + } + ); + + const colorFn = (arg) => arg; + mockery.registerMock('ansi-colors', { + green: colorFn, + yellow: colorFn, + magenta: colorFn, + cyan: colorFn, + red: colorFn, + gray: colorFn, + grey: colorFn + }); + + mockery.registerMock('../constants', { + AVAILABLE_SUBCOMMANDS: { + subcmd1: { + flags: [{ + name: 'flag1', + cliConfigs: [{ + name: 'config1', + alias: [] + }] + }] + } + } + }); + + const {verifyOptions} = require('../../../../../src/commands/android/subcommands/common'); + const result = verifyOptions('subcmd1', {flag1: true, random: true}); + const output = consoleOutput.toString(); + assert.strictEqual(output.includes('Unknown config(s) passed for \'--flag1\' flag: random'), true); + assert.strictEqual(output.includes('(none expected)'), false); + assert.deepStrictEqual(result, false); + }); + + it('works for correct flag and correct corresponding configs', () => { + const consoleOutput = []; + mockery.registerMock( + '../../../logger', + class { + static log(...msgs) { + consoleOutput.push(...msgs); + } + } + ); + + const colorFn = (arg) => arg; + mockery.registerMock('ansi-colors', { + green: colorFn, + yellow: colorFn, + magenta: colorFn, + cyan: colorFn, + red: colorFn, + gray: colorFn, + grey: colorFn + }); + + mockery.registerMock('../constants', { + AVAILABLE_SUBCOMMANDS: { + subcmd1: { + flags: [{ + name: 'flag1', + cliConfigs: [{ + name: 'config1', + alias: ['c1'] + }] + }] + } + } + }); + + const {verifyOptions} = require('../../../../../src/commands/android/subcommands/common'); + const result1 = verifyOptions('subcmd1', {flag1: true, config1: true}); + const output1 = consoleOutput.toString(); + assert.strictEqual(output1.includes('Unknown config(s) passed'), false); + assert.deepStrictEqual(result1, {subcommandFlag: 'flag1', configs: ['config1']}); + + consoleOutput.length = 0; + const options = {flag1: true, c1: true}; + + const result2 = verifyOptions('subcmd1', options); + const output2 = consoleOutput.toString(); + assert.strictEqual(output2.includes('Unknown config(s) passed'), false); + assert.deepStrictEqual(result2, {subcommandFlag: 'flag1', configs: ['c1']}); + + // main config gets added to options for aliases + assert.deepStrictEqual(options, {flag1: true, c1: true, config1: true}); + }); + // --- (allowedFlags.length > 0) ends here --- + + // --- (allowedFlags.length = 0) starts here --- + it('shows error for unknown config (allowedConfigs = 0)', () => { + const consoleOutput = []; + mockery.registerMock( + '../../../logger', + class { + static log(...msgs) { + consoleOutput.push(...msgs); + } + } + ); + + const colorFn = (arg) => arg; + mockery.registerMock('ansi-colors', { + green: colorFn, + yellow: colorFn, + magenta: colorFn, + cyan: colorFn, + red: colorFn, + gray: colorFn, + grey: colorFn + }); + + mockery.registerMock('../constants', { + AVAILABLE_SUBCOMMANDS: { + subcmd1: { + flags: [], + cliConfigs: [] + } + } + }); + + const {verifyOptions} = require('../../../../../src/commands/android/subcommands/common'); + const result = verifyOptions('subcmd1', {random: true}); + const output = consoleOutput.toString(); + assert.strictEqual(output.includes('Unknown config(s) passed for \'subcmd1\' subcommand: random (none expected)'), true); + assert.deepStrictEqual(result, false); + }); + + it('shows error for unknown config (allowedConfigs > 0)', () => { + const consoleOutput = []; + mockery.registerMock( + '../../../logger', + class { + static log(...msgs) { + consoleOutput.push(...msgs); + } + } + ); + + const colorFn = (arg) => arg; + mockery.registerMock('ansi-colors', { + green: colorFn, + yellow: colorFn, + magenta: colorFn, + cyan: colorFn, + red: colorFn, + gray: colorFn, + grey: colorFn + }); + + mockery.registerMock('../constants', { + AVAILABLE_SUBCOMMANDS: { + subcmd1: { + flags: [], + cliConfigs: [{ + name: 'config1', + alias: [] + }] + } + } + }); + + const {verifyOptions} = require('../../../../../src/commands/android/subcommands/common'); + const result = verifyOptions('subcmd1', {random: true}); + const output = consoleOutput.toString(); + assert.strictEqual(output.includes('Unknown config(s) passed for \'subcmd1\' subcommand: random'), true); + assert.strictEqual(output.includes('(none expected)'), false); + assert.deepStrictEqual(result, false); + }); + + it('works for correct configs corresponding to the subcommand', () => { + const consoleOutput = []; + mockery.registerMock( + '../../../logger', + class { + static log(...msgs) { + consoleOutput.push(...msgs); + } + } + ); + + const colorFn = (arg) => arg; + mockery.registerMock('ansi-colors', { + green: colorFn, + yellow: colorFn, + magenta: colorFn, + cyan: colorFn, + red: colorFn, + gray: colorFn, + grey: colorFn + }); + + mockery.registerMock('../constants', { + AVAILABLE_SUBCOMMANDS: { + subcmd1: { + flags: [], + cliConfigs: [{ + name: 'config1', + alias: ['c1'] + }] + } + } + }); + + const {verifyOptions} = require('../../../../../src/commands/android/subcommands/common'); + const result1 = verifyOptions('subcmd1', {config1: true}); + const output1 = consoleOutput.toString(); + assert.strictEqual(output1.includes('Unknown config(s) passed'), false); + assert.deepStrictEqual(result1, {subcommandFlag: '', configs: ['config1']}); + + consoleOutput.length = 0; + const options = {c1: 'something'}; + const result2 = verifyOptions('subcmd1', options); + const output2 = consoleOutput.toString(); + assert.strictEqual(output2.includes('Unknown config(s) passed'), false); + assert.deepStrictEqual(result2, {subcommandFlag: '', configs: ['c1']}); + + // main config gets added to options for aliases + assert.deepStrictEqual(options, {c1: 'something', config1: 'something'}); + }); + // --- (allowedFlags.length = 0) ends here --- +}); From 39bda375579f3f4b6063aa48662eb09d20eb8731 Mon Sep 17 00:00:00 2001 From: Priyansh Prajapati <88396544+itsspriyansh@users.noreply.github.com> Date: Mon, 26 Aug 2024 17:04:24 +0530 Subject: [PATCH 08/10] feat: uninstall app (#59) --- src/commands/android/constants.ts | 12 ++ .../android/subcommands/uninstall/app.ts | 122 ++++++++++++++++++ .../android/subcommands/uninstall/index.ts | 36 +++++- 3 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 src/commands/android/subcommands/uninstall/app.ts diff --git a/src/commands/android/constants.ts b/src/commands/android/constants.ts index 55d923e..7f3d3d4 100644 --- a/src/commands/android/constants.ts +++ b/src/commands/android/constants.ts @@ -84,6 +84,18 @@ export const AVAILABLE_SUBCOMMANDS: AvailableSubcommands = { description: 'todo item', flags: [ {name: 'avd', description: 'todo item'}, + { + name: 'app', + description: 'Uninstall an APK from a device', + cliConfigs: [ + { + name: 'deviceId', + alias: ['s'], + description: 'Id of the device to uninstall the APK from if multiple devices are connected', + usageHelp: 'device_id' + } + ] + } ] } }; diff --git a/src/commands/android/subcommands/uninstall/app.ts b/src/commands/android/subcommands/uninstall/app.ts new file mode 100644 index 0000000..ddd4e47 --- /dev/null +++ b/src/commands/android/subcommands/uninstall/app.ts @@ -0,0 +1,122 @@ +import colors from 'ansi-colors'; +import inquirer from 'inquirer'; + +import Logger from '../../../../logger'; +import {Options, Platform} from '../../interfaces'; +import ADB from '../../utils/appium-adb'; +import {getBinaryLocation} from '../../utils/common'; +import {execBinaryAsync, execBinarySync} from '../../utils/sdk'; +import {showMissingBinaryHelp} from '../common'; + +export async function uninstallApp(options: Options, sdkRoot: string, platform: Platform): Promise { + try { + const adbLocation = getBinaryLocation(sdkRoot, platform, 'adb', true); + if (!adbLocation) { + showMissingBinaryHelp('adb'); + + return false; + } + + const adb = await ADB.createADB({allowOfflineDevices: true}); + const devices = await adb.getConnectedDevices(); + + if (!devices.length) { + Logger.log(`${colors.red('No device found running.')} Please connect the device to uninstall the app from.\n`); + + return true; + } + + if (options.deviceId) { + // If device id is passed then check if the id is valid. If not then prompt user to select a device. + const deviceConnected = devices.find(device => device.udid === options.deviceId); + if (!deviceConnected) { + Logger.log(colors.yellow(`No connected device found with deviceId '${options.deviceId}'.\n`)); + + options.deviceId = ''; + } + } else if (devices.length === 1) { + options.deviceId = devices[0].udid; + } + + if (!options.deviceId) { + // if device id not found, or invalid device id is found, then prompt the user + // to select a device from the list of running devices. + const deviceAnswer = await inquirer.prompt({ + type: 'list', + name: 'device', + message: 'Select the device to uninstall the APK from:', + choices: devices.map(device => device.udid) + }); + options.deviceId = deviceAnswer.device; + } + + const appNameAnswer = await inquirer.prompt({ + type: 'input', + name: 'appName', + message: `Name of the app to uninstall from device '${options.deviceId}':` + }); + const appName = appNameAnswer.appName; + + const packageNames = execBinarySync(adbLocation, 'adb', platform, `-s ${options.deviceId} shell pm list packages '${appName}'`); + if (!packageNames) { + Logger.log(); + Logger.log(`${colors.red(`No package found with name '${appName}'!`)} Please try again.\n`); + + return false; + } + + const packagesList: string[] = []; + // Name of a package is in the format 'package:com.example.app' + packageNames.split('\n').forEach(line => { + if (line.includes('package:')) { + packagesList.push(line.split(':')[1].trim()); + } + }); + + let packageName = packagesList[0]; + if (packagesList.length > 1) { + const packageNameAnswer = await inquirer.prompt({ + type: 'list', + name: 'packageName', + message: 'Select the package you want to uninstall:', + choices: packagesList + }); + packageName = packageNameAnswer.packageName; + } + + const uninstallationConfirmation = await inquirer.prompt({ + type: 'confirm', + name: 'confirm', + message: `Are you sure you want to uninstall ${colors.cyan(packageName)}` + }); + + Logger.log(); + + if (!uninstallationConfirmation.confirm) { + Logger.log('Uninstallation cancelled.\n'); + + return false; + } + + Logger.log(`Uninstalling ${colors.cyan(packageName)}...\n`); + + const uninstallationStatus = await execBinaryAsync(adbLocation, 'adb', platform, `-s ${options.deviceId} uninstall ${packageName}`); + if (uninstallationStatus?.includes('Success')) { + Logger.log(`${colors.green('App uninstalled successfully!')}\n`); + + return true; + } + + Logger.log(colors.red('Something went wrong while uninstalling app.')); + Logger.log('Command output:', uninstallationStatus); + Logger.log('Please check the output above and try again.'); + + return false; + } catch (error) { + Logger.log(colors.red('Error occurred while uninstalling app.')); + console.error(error); + + return false; + } +} + diff --git a/src/commands/android/subcommands/uninstall/index.ts b/src/commands/android/subcommands/uninstall/index.ts index 0947987..2d5333a 100644 --- a/src/commands/android/subcommands/uninstall/index.ts +++ b/src/commands/android/subcommands/uninstall/index.ts @@ -1,11 +1,45 @@ +import inquirer from 'inquirer'; + +import Logger from '../../../../logger'; import {Options, Platform} from '../../interfaces'; +import {verifyOptions} from '../common'; +import {uninstallApp} from './app'; import {deleteAvd} from './avd'; export async function uninstall(options: Options, sdkRoot: string, platform: Platform): Promise { - if (options.avd) { + const optionsVerified = verifyOptions('uninstall', options); + if (!optionsVerified) { + return false; + } + + let subcommandFlag = optionsVerified.subcommandFlag; + if (subcommandFlag === '') { + subcommandFlag = await promptForFlag(); + } + + if (subcommandFlag === 'app') { + return await uninstallApp(options, sdkRoot, platform); + } else if (subcommandFlag === 'avd') { return await deleteAvd(sdkRoot, platform); } return false; } +async function promptForFlag(): Promise { + const flagAnswer = await inquirer.prompt({ + type: 'list', + name: 'flag', + message: 'Select what do you want to uninstall:', + choices: ['App', 'AVD'] + }); + Logger.log(); + + const flag = flagAnswer.flag; + if (flag === 'App') { + return 'app'; + } + + return 'avd'; +} + From 1736a7c9c3e5df5bec6db95718a0df64712aab5f Mon Sep 17 00:00:00 2001 From: Priyansh Garg Date: Mon, 26 Aug 2024 17:06:22 +0530 Subject: [PATCH 09/10] Refactor flag prompt for uninstall subcommand. --- src/commands/android/subcommands/uninstall/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/android/subcommands/uninstall/index.ts b/src/commands/android/subcommands/uninstall/index.ts index 2d5333a..d302994 100644 --- a/src/commands/android/subcommands/uninstall/index.ts +++ b/src/commands/android/subcommands/uninstall/index.ts @@ -30,13 +30,13 @@ async function promptForFlag(): Promise { const flagAnswer = await inquirer.prompt({ type: 'list', name: 'flag', - message: 'Select what do you want to uninstall:', - choices: ['App', 'AVD'] + message: 'Select what you want to uninstall:', + choices: ['Android App', 'Android Virtual Device (AVD)'] }); Logger.log(); const flag = flagAnswer.flag; - if (flag === 'App') { + if (flag === 'Android App') { return 'app'; } From f4304f1f9d16adcebe27dfafcba1a52184a873e4 Mon Sep 17 00:00:00 2001 From: Priyansh Prajapati <88396544+itsspriyansh@users.noreply.github.com> Date: Tue, 3 Sep 2024 23:35:44 +0530 Subject: [PATCH 10/10] doc: subcommand usage workflows (#69) Co-authored-by: Priyansh Garg --- docs/subcommands/connect.md | 15 +++++++++++++++ docs/subcommands/install.md | 32 ++++++++++++++++++++++++++++++++ docs/subcommands/list.md | 23 +++++++++++++++++++++++ docs/subcommands/uninstall.md | 15 +++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 docs/subcommands/connect.md create mode 100644 docs/subcommands/install.md create mode 100644 docs/subcommands/list.md create mode 100644 docs/subcommands/uninstall.md diff --git a/docs/subcommands/connect.md b/docs/subcommands/connect.md new file mode 100644 index 0000000..10ebab7 --- /dev/null +++ b/docs/subcommands/connect.md @@ -0,0 +1,15 @@ +## Workflows of the `connect` subcommand + +**Syntax** +```sh +npx @nightwatch/mobile-helper android connect FLAG [configs] +``` + +### 1. Connect a real device wirelessly + +> Note: Only devices with Android version 11 or higher are supported. + +Run the below command to connect to a real device wirelessly: +```sh +npx @nightwatch/mobile-helper android connect --wireless +``` diff --git a/docs/subcommands/install.md b/docs/subcommands/install.md new file mode 100644 index 0000000..a219281 --- /dev/null +++ b/docs/subcommands/install.md @@ -0,0 +1,32 @@ +## Workflows of the `install` subcommand + +**Syntax** +```sh +npx @nightwatch/mobile-helper android install FLAG [configs] +``` + +### 1. Install an APK + +Run the below command to install an APK on a real device or an AVD: + +```sh +npx @nightwatch/mobile-helper android install --app + +# with configs +npx @nightwatch/mobile-helper android install --app [--deviceId ] [--path ] +``` + +**Configs** + +| Config | Description | +| ------------------------------ | -------------------------------------------------------------- | +| --deviceId \| -s | Id of the device to install the APK to | +| --path \| -p | Path to the APK file relative to the current working directory | + +### 2. Create a new Android Virtual Device + +Run the below command to create a new AVD: + +```sh +npx @nightwatch/mobile-helper android install --avd +``` diff --git a/docs/subcommands/list.md b/docs/subcommands/list.md new file mode 100644 index 0000000..0bc4edd --- /dev/null +++ b/docs/subcommands/list.md @@ -0,0 +1,23 @@ +## Workflows of the `list` subcommand + +**Syntax** + +```sh +npx @nightwatch/mobile-helper android list [flags] +``` + +### 1. Show a list of connected devices + +Run the below command to show a list of all the connected real devices and running AVDs: + +```sh +npx @nightwatch/mobile-helper android list --device +``` + +### 2. Show a list of installed AVDs + +Run the below command to show a list of all the currently installed AVDs + +```sh +npx @nightwatch/mobile-helper android list --avd +``` diff --git a/docs/subcommands/uninstall.md b/docs/subcommands/uninstall.md new file mode 100644 index 0000000..d3a390c --- /dev/null +++ b/docs/subcommands/uninstall.md @@ -0,0 +1,15 @@ +## Workflows of the `uninstall` subcommand + +**Syntax** + +```sh +npx @nightwatch/mobile-helper android uninstall FLAG [configs] +``` + +### 1. Delete an Android Virtual Device (AVD) + +Run the below command to delete an AVD: + +```sh +npx @nightwatch/mobile-helper android uninstall --avd +```