Skip to content

Commit

Permalink
feat: added scripts and ability to handle subcommands
Browse files Browse the repository at this point in the history
  • Loading branch information
itsspriyansh committed Jun 2, 2024
1 parent 7053c19 commit 400b9fe
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 36 deletions.
29 changes: 24 additions & 5 deletions src/commands/android/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import inquirer from 'inquirer';
import path from 'path';
import os from 'os';

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

export const AVAILABLE_OPTIONS: AvailableOptions = {
help: {
Expand All @@ -29,12 +29,31 @@ export const AVAILABLE_OPTIONS: AvailableOptions = {
alias: [],
description: 'Do standalone setup for Android Emulator (no Nightwatch-related requirements will be downloaded).'
},
wireless: {
alias: [],
description: 'Connect ADB with wireless connection.'
}
};

export const AVAILABLE_SUBCOMMANDS: AvailableSubcommands = {
connect: {
description: 'Connect to a device',
options: [
{
name: "wireless",
description: "Connect a real device wirelessly",
},
{
name: "avd",
description: "Connect to an Android Virtual Device",
},
{
name: "list",
description: "List all connected devices",
}
]
},
disconnect: {
description: 'Disconnect an AVD or a real device',
}
}

export const NIGHTWATCH_AVD = 'nightwatch-android-11';
export const DEFAULT_FIREFOX_VERSION = '105.1.0';
export const DEFAULT_CHROME_VERSIONS = ['83', '91'];
Expand Down
77 changes: 62 additions & 15 deletions src/commands/android/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,38 @@ import Logger from '../../logger';
import {getPlatformName, symbols} from '../../utils';
import {getAlreadyRunningAvd, killEmulatorWithoutWait, launchAVD} from './adb';
import {
ABI, AVAILABLE_OPTIONS, BINARY_TO_PACKAGE_NAME, DEFAULT_CHROME_VERSIONS,
ABI, AVAILABLE_OPTIONS, AVAILABLE_SUBCOMMANDS, BINARY_TO_PACKAGE_NAME, DEFAULT_CHROME_VERSIONS,
DEFAULT_FIREFOX_VERSION, NIGHTWATCH_AVD, SETUP_CONFIG_QUES
} from './constants';
import {AndroidSetupResult, Options, OtherInfo, Platform, SdkBinary, SetupConfigs} from './interfaces';
import {AndroidSetupResult, Options, OtherInfo, Platform, SdkBinary, SetupConfigs } from './interfaces';
import {
downloadFirefoxAndroid, downloadWithProgressBar, getAllAvailableOptions,
getBinaryLocation, getBinaryNameForOS, getFirefoxApkName, getLatestVersion
downloadFirefoxAndroid, downloadWithProgressBar,
getBinaryLocation, getBinaryNameForOS, getFirefoxApkName, getLatestVersion,
getUnknownOptions,
} from './utils/common';
import {
downloadAndSetupAndroidSdk, downloadSdkBuildTools, execBinarySync,
getBuildToolsAvailableVersions, getDefaultAndroidSdkRoot, installPackagesUsingSdkManager
} from './utils/sdk';

import DOWNLOADS from './downloads.json';
import {connectWirelessAdb} from './utils/adbWirelessConnect';
import {connectAvd, connectWirelessAdb, defaultConnectFlow, disconnectDevice, listRunningDevices} from './utils/connectDevice';


export class AndroidSetup {
sdkRoot: string;
javaHome: string;
options: Options;
subcommand: string;
rootDir: string;
platform: Platform;
otherInfo: OtherInfo;

constructor(options: Options = {}, rootDir = process.cwd()) {
constructor(options: Options = {}, subcommand: string, rootDir = process.cwd()) {
this.sdkRoot = '';
this.javaHome = '';
this.options = options;
this.subcommand = subcommand;
this.rootDir = rootDir;
this.platform = getPlatformName();
this.otherInfo = {
Expand All @@ -50,11 +53,15 @@ export class AndroidSetup {
}

async run(): Promise<AndroidSetupResult | boolean> {
const allAvailableOptions = getAllAvailableOptions();
const unknownOptions = Object.keys(this.options).filter((option) => !allAvailableOptions.includes(option));
const unknownOptions = getUnknownOptions(this.options, this.subcommand);

if (this.options.help || unknownOptions.length) {
this.showHelp(unknownOptions);
let unknownSubcommand;
if (this.subcommand && !Object.keys(AVAILABLE_SUBCOMMANDS).includes(this.subcommand)) {
unknownSubcommand = this.subcommand;
}

if (this.options.help || unknownOptions.length || unknownSubcommand) {
this.showHelp(unknownOptions, unknownSubcommand);

return this.options.help === true;
}
Expand Down Expand Up @@ -108,8 +115,8 @@ export class AndroidSetup {
this.sdkRoot = sdkRootEnv || await this.getSdkRootFromUser();
process.env.ANDROID_HOME = this.sdkRoot;

if (this.options.wireless) {
return await connectWirelessAdb(this.sdkRoot, this.platform);
if (this.subcommand) {
return this.executeSdkScript(this.subcommand, this.options);
}

let result = true;
Expand Down Expand Up @@ -155,12 +162,14 @@ export class AndroidSetup {
};
}

showHelp(unknownOptions: string[]) {
if (unknownOptions.length) {
showHelp(unknownOptions: string[], unknownSubcommand: string | undefined) {
if (unknownSubcommand) {
Logger.log(colors.red(`unknown subcommand passed: ${unknownSubcommand}\n`));
} else if (unknownOptions.length) {
Logger.log(colors.red(`unknown option(s) passed: ${unknownOptions.join(', ')}\n`));
}

Logger.log(`Usage: ${colors.cyan('npx @nightwatch/mobile-helper android [options]')}`);
Logger.log(`Usage: ${colors.cyan('npx @nightwatch/mobile-helper android [options] [subcommand] [subcommand-options]')}`);
Logger.log(' Verify if all the requirements are met to run tests on an Android device/emulator.\n');

Logger.log(`${colors.yellow('Options:')}`);
Expand Down Expand Up @@ -199,6 +208,26 @@ export class AndroidSetup {

Logger.log(prelude + ' ' + colors.grey(desc));
});

Logger.log(`\n${colors.yellow('Subcommands and Subcommand-Options:')}`);

Object.keys(AVAILABLE_SUBCOMMANDS).forEach(subcommand => {
const subcmd = AVAILABLE_SUBCOMMANDS[subcommand];
const subcmdOptions = subcmd.options?.map(option => `[--${option.name}]`).join(' ') || '';

Logger.log(` ${colors.cyan(subcommand)} ${subcmdOptions}`);
Logger.log(` ${colors.gray(subcmd.description)}`);

if (subcmd.options && subcmd.options.length > 0) {
const optionLongest = longest(subcmd.options.map(option => `--${option.name}`));
subcmd.options.forEach(option => {
const optionStr = `--${option.name}`;
const optionPadding = new Array(Math.max(optionLongest - optionStr.length + 3, 0)).join('.');
Logger.log(` ${optionStr} ${colors.grey(optionPadding)} ${colors.gray(option.description)}`);
});
}
Logger.log();
});
}

checkJavaInstallation(): boolean {
Expand Down Expand Up @@ -1071,4 +1100,22 @@ export class AndroidSetup {
envSetHelp() {
// Add platform-wise help or link a doc to help users set env variable.
}

async executeSdkScript (subcommand: string, options: Options) {
if (subcommand === 'connect') {
if (options.wireless) {
return await connectWirelessAdb(this.sdkRoot, this.platform);
} else if (options.avd) {
return await connectAvd(this.sdkRoot, this.platform);
} else if (options.list) {
return listRunningDevices();
} else {
return await defaultConnectFlow(this.sdkRoot, this.platform);
}
}
if (subcommand === 'disconnect') {
return await disconnectDevice(this.sdkRoot, this.platform);
}
return false;
}
}
12 changes: 12 additions & 0 deletions src/commands/android/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ export interface Options {
[key: string]: string | string[] | boolean;
}

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

export type Subcommand = 'connect' | 'disconnect' | undefined;

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

export interface OtherInfo {
Expand Down
18 changes: 13 additions & 5 deletions src/commands/android/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,25 @@ import path from 'path';
import which from 'which';

import {symbols} from '../../../utils';
import {ABI, AVAILABLE_OPTIONS, DEFAULT_CHROME_VERSIONS, DEFAULT_FIREFOX_VERSION, SDK_BINARY_LOCATIONS} from '../constants';
import {Platform, SdkBinary} from '../interfaces';
import {ABI, AVAILABLE_OPTIONS, AVAILABLE_SUBCOMMANDS, DEFAULT_CHROME_VERSIONS, DEFAULT_FIREFOX_VERSION, SDK_BINARY_LOCATIONS} from '../constants';
import { Options, Platform, SdkBinary} from '../interfaces';

export const getAllAvailableOptions = () => {
export const getUnknownOptions = (options: Options, subcommand: string) => {
const mainOptions = Object.keys(AVAILABLE_OPTIONS);

const allOptions: string[] = [];

mainOptions.forEach((option) => allOptions.push(option, ...AVAILABLE_OPTIONS[option].alias));
let unknownOptions = Object.keys(options).filter((option) => !allOptions.includes(option));

return allOptions;
};
if (subcommand) {
// if a subcommand is provided then remove corresponding subcommand's options from `unknownOptions`.
const subcommandOptions = AVAILABLE_SUBCOMMANDS[subcommand]?.options?.map((option) => option.name);
unknownOptions = unknownOptions.filter((option) => !subcommandOptions?.includes(option));
}

return unknownOptions;
}

export const getBinaryNameForOS = (platform: Platform, binaryName: string) => {
if (platform !== 'windows') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import Logger from '../../../logger';
import inquirer from 'inquirer';
import colors from 'ansi-colors';
import { getBinaryLocation } from './common';
import { launchAVD } from '../adb';
import ADB from 'appium-adb';

export async function connectWirelessAdb(sdkRoot: string, platform: Platform): Promise<boolean> {
try {
Expand Down Expand Up @@ -77,7 +79,7 @@ export async function connectWirelessAdb(sdkRoot: string, platform: Platform): P
}

const connecting = execBinarySync(adbLocation, 'adb', platform, `connect ${deviceIP}:${port}`);
if (connecting) {
if (connecting?.includes('connected')) {
Logger.log(colors.green('Connected to device wirelessly.'));
} else {
Logger.log(`${colors.red('Failed to connect!')} Please try again.`);
Expand All @@ -93,3 +95,114 @@ export async function connectWirelessAdb(sdkRoot: string, platform: Platform): P
return false;
}
}

export async function connectAvd (sdkRoot: string, platform: Platform): Promise<boolean> {
const avdmanagerLocation = getBinaryLocation(sdkRoot, platform, 'avdmanager', true);
const availableAVDsDetails = execBinarySync(avdmanagerLocation, 'avdmanager', platform, 'list avd');

if (availableAVDsDetails) {
Logger.log('Available AVDs:');
Logger.log(availableAVDsDetails);
}

const availableAVDs = execBinarySync(avdmanagerLocation, 'avdmanager', platform, 'list avd -c');

if (availableAVDs) {
const avdAnswer = await inquirer.prompt({
type: 'list',
name: 'avdName',
message: 'Select the AVD to connect:',
choices: availableAVDs.split('\n').filter(avd => avd !== '')
});
const avdName = avdAnswer.avdName;

Logger.log(`Connecting to ${avdName}...`);
const connectingAvd = await launchAVD(sdkRoot, platform, avdName);

if (connectingAvd) {
return true;
}

return false;
} else {
Logger.log(`${colors.red('No AVDs found!')} Use ${colors.magenta('--setup')} flag with the main command to setup missing requirements.`);
}
return false;
}

export async function disconnectDevice (sdkRoot: string, platform: Platform) {
const adbLocation = getBinaryLocation(sdkRoot, platform, 'adb', true);
const adb = await ADB.createADB({allowOfflineDevices: true});
const devices = await adb.getConnectedDevices();

if (devices.length === 0) {
Logger.log(`${colors.yellow('No device found running.')}`);
return true;
}

const deviceAnswer = await inquirer.prompt({
type: 'list',
name: 'device',
message: 'Select the device to disconnect:',
choices: devices.map(device => device.udid)
});
const deviceId = deviceAnswer.device;

let disconnecting;
if (deviceId.includes('emulator')) {
disconnecting = execBinarySync(adbLocation, 'adb', platform, `-s ${deviceId} emu kill`);
} else {
disconnecting = execBinarySync(adbLocation, 'adb', platform, `disconnect ${deviceId}`);
}
console.log(disconnecting);

return false;
}

export async function listRunningDevices() {
const adb = await ADB.createADB({ allowOfflineDevices: true });
const devices = await adb.getConnectedDevices();

if (devices.length === 0) {
Logger.log(`No device connected.`);
return true;
}

Logger.log(colors.bold('Connected Devices:'));

const maxUdidLength = devices.reduce((max, device) => Math.max(max, device.udid.length), 0);
const paddedLength = maxUdidLength + 2;

devices.forEach((device) => {
const paddedUdid = device.udid.padEnd(paddedLength);
Logger.log(`${paddedUdid}${device.state}`);
});

return true;
}

export async function defaultConnectFlow(sdkRoot: string, platform: Platform) {
await listRunningDevices();

Logger.log();

const connectAnswer = await inquirer.prompt({
type: 'list',
name: 'connectOption',
message: 'Select the type of device to connect:',
choices: ['Real Device', 'AVD']
});
const connectOption = connectAnswer.connectOption;

Logger.log();

switch (connectOption) {
case 'Real Device':
return await connectWirelessAdb(sdkRoot, platform);
case 'AVD':
return await connectAvd(sdkRoot, platform);
default:
Logger.log('Invalid option selected.');
return false;
}
}
Loading

0 comments on commit 400b9fe

Please sign in to comment.