diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index 058a465f7..0e2d335dc 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -76,6 +76,7 @@ jobs: cd ~ npm install -g appium appium driver install --source=local "$cwd" + appium driver doctor xcuitest appium driver run xcuitest build-wda echo "Starting Appium server on $APPIUM_TEST_SERVER_HOST:$APPIUM_TEST_SERVER_PORT" nohup appium server \ diff --git a/docs/setup.md b/docs/setup.md index 3c44bd6b5..d8d1fdb27 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -42,6 +42,12 @@ On top of standard Appium requirements XCUITest driver also expects the followin - [WIX AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils) could be used to improve some Simulator interactions - [py-ios-device](https://github.com/YueChen-C/py-ios-device) is required in several `mobile:` extensions and to improve the general testing experience for _real_ iOS devices +### Doctor + +Since driver version 5.13.0 you can automate the validation for the most of the above +requirements as well as various optional ones needed by driver extensions by running the +`appium driver doctor xcuitest` server command. + ## Xcode version support diff --git a/lib/doctor/optional-checks.js b/lib/doctor/optional-checks.js new file mode 100644 index 000000000..bcccabaf1 --- /dev/null +++ b/lib/doctor/optional-checks.js @@ -0,0 +1,94 @@ +/* eslint-disable require-await */ +import {resolveExecutablePath} from './utils'; +import {doctor} from '@appium/support'; +import '@colors/colors'; + +/** @satisfies {import('@appium/types').IDoctorCheck} */ +export class OptionalIdbCommandCheck { + IDB_README_URL = 'https://git.io/JnxQc'; + + async diagnose() { + const fbIdbPath = await resolveExecutablePath('idb'); + const fbCompanionIdbPath = await resolveExecutablePath('idb_companion'); + if (fbIdbPath && fbCompanionIdbPath) { + return doctor.okOptional('idb and idb_companion are installed'); + } + + if (!fbIdbPath && fbCompanionIdbPath) { + return doctor.nokOptional('idb is not installed'); + } else if (fbIdbPath && !fbCompanionIdbPath) { + return doctor.nokOptional('idb_companion is not installed'); + } + return doctor.nokOptional('idb and idb_companion are not installed'); + } + + async fix() { + return `Why ${'idb'.bold} is needed and how to install it: ${this.IDB_README_URL}`; + } + + hasAutofix() { + return false; + } + + isOptional() { + return true; + } +} +export const optionalIdbCheck = new OptionalIdbCommandCheck(); + + +/** @satisfies {import('@appium/types').IDoctorCheck} */ +export class OptionalApplesimutilsCommandCheck { + README_LINK = 'https://github.com/appium/appium-xcuitest-driver/blob/master/docs/execute-methods.md#mobile-setpermission'; + + async diagnose() { + const applesimutilsPath = await resolveExecutablePath('applesimutils'); + return applesimutilsPath + ? doctor.okOptional(`applesimutils is installed at: ${applesimutilsPath}`) + : doctor.nokOptional('applesimutils are not installed'); + } + + async fix() { + return `Why ${'applesimutils'.bold} is needed and how to install it: ${this.README_LINK}`; + } + + hasAutofix() { + return false; + } + + isOptional() { + return true; + } +} +export const optionalApplesimutilsCheck = new OptionalApplesimutilsCommandCheck(); + + +/** @satisfies {import('@appium/types').IDoctorCheck} */ +export class OptionalFfmpegCheck { + FFMPEG_BINARY = 'ffmpeg'; + FFMPEG_INSTALL_LINK = 'https://www.ffmpeg.org/download.html'; + + async diagnose() { + const ffmpegPath = await resolveExecutablePath(this.FFMPEG_BINARY); + + return ffmpegPath + ? doctor.okOptional(`${this.FFMPEG_BINARY} exists at '${ffmpegPath}'`) + : doctor.nokOptional(`${this.FFMPEG_BINARY} cannot be found`); + } + + async fix() { + return ( + `${`${this.FFMPEG_BINARY}`.bold} is used to capture screen recordings from the device under test. ` + + `Please read ${this.FFMPEG_INSTALL_LINK}.` + ); + } + + hasAutofix() { + return false; + } + + isOptional() { + return true; + } +} +export const optionalFfmpegCheck = new OptionalFfmpegCheck(); diff --git a/lib/doctor/required-checks.js b/lib/doctor/required-checks.js new file mode 100644 index 000000000..e062c4b36 --- /dev/null +++ b/lib/doctor/required-checks.js @@ -0,0 +1,124 @@ +/* eslint-disable require-await */ +import {fs, doctor} from '@appium/support'; +import {exec} from 'teen_process'; +import { getPath as getXcodePath } from 'appium-xcode'; +import '@colors/colors'; + + +/** @satisfies {import('@appium/types').IDoctorCheck} */ +export class XcodeCheck { + async diagnose() { + try { + const xcodePath = await getXcodePath(); + return doctor.ok(`xCode is installed at '${xcodePath}'`); + } catch (err) { + return doctor.nok(err.message); + } + } + + async fix() { + return `Manually install ${'Xcode'.bold} and configure the active developer directory path using the xcode-select tool`; + } + + hasAutofix() { + return false; + } + + isOptional() { + return false; + } +} +export const xcodeCheck = new XcodeCheck(); + + +/** @satisfies {import('@appium/types').IDoctorCheck} */ +export class XcodeToolsCheck { + async diagnose() { + try { + // https://github.com/appium/appium/issues/12093#issuecomment-459358120 + await exec('xcrun', ['simctl', 'help']); + } catch (err) { + return doctor.nok(`Cannot run 'xcrun simctl': ${err.stderr || err.message}`); + } + try { + await exec('xcodebuild', ['-version']); + } catch (err) { + return doctor.nok(`Cannot run 'xcodebuild': ${err.stderr || err.message}`); + } + return doctor.ok(`xCode tools are installed and work properly`); + } + + async fix() { + return `Fix the problems xCode tools are compliaining about`; + } + + hasAutofix() { + return false; + } + + isOptional() { + return false; + } +} +export const xcodeToolsCheck = new XcodeToolsCheck(); + + +/** + * @typedef EnvVarCheckOptions + * @property {boolean} [expectDir] If set to true then + * the path is expected to be a valid folder + * @property {boolean} [expectFile] If set to true then + * the path is expected to be a valid file + */ + +/** @satisfies {import('@appium/types').IDoctorCheck} */ +class EnvVarAndPathCheck { + ENVIRONMENT_VARS_TUTORIAL_URL = 'https://github.com/appium/java-client/blob/master/docs/environment.md'; + + /** + * @param {string} varName + * @param {EnvVarCheckOptions} [opts={}] + */ + constructor(varName, opts = {}) { + this.varName = varName; + this.opts = opts; + } + + async diagnose() { + const varValue = process.env[this.varName]; + if (!varValue) { + return doctor.nok(`${this.varName} environment variable is NOT set!`); + } + + if (!await fs.exists(varValue)) { + let errMsg = `${this.varName} is set to '${varValue}' but this path does not exist!`; + return doctor.nok(errMsg); + } + + const stat = await fs.stat(varValue); + if (this.opts.expectDir && !stat.isDirectory()) { + return doctor.nok(`${this.varName} is expected to be a valid folder, got a file path instead`); + } + if (this.opts.expectFile && stat.isDirectory()) { + return doctor.nok(`${this.varName} is expected to be a valid file, got a folder path instead`); + } + + return doctor.ok(`${this.varName} is set to: ${varValue}`); + } + + async fix() { + return ( + `Make sure the environment variable ${this.varName.bold} is properly configured for the Appium process. ` + + `Refer ${this.ENVIRONMENT_VARS_TUTORIAL_URL} for more details.` + ); + } + + hasAutofix() { + return false; + } + + isOptional() { + return false; + } +} +export const homeEnvVarCheck = new EnvVarAndPathCheck('HOME', {expectDir: true}); diff --git a/lib/doctor/utils.js b/lib/doctor/utils.js new file mode 100644 index 000000000..bac53a9bb --- /dev/null +++ b/lib/doctor/utils.js @@ -0,0 +1,17 @@ +import {fs} from '@appium/support'; + +/** + * Return an executable path of cmd + * + * @param {string} cmd Standard output by command + * @return {Promise} The full path of cmd. `null` if the cmd is not found. + */ +export async function resolveExecutablePath(cmd) { + try { + const executablePath = await fs.which(cmd); + if (executablePath && (await fs.exists(executablePath))) { + return executablePath; + } + } catch (err) {} + return null; +} diff --git a/package.json b/package.json index a2c6543c3..4b806a2f7 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,12 @@ "additionalProperties": false, "title": "XCUITest Driver Configuration", "description": "Appium configuration schema for the XCUITest driver." + }, + "doctor": { + "checks": [ + "./build/lib/doctor/required-checks.js", + "./build/lib/doctor/optional-checks.js" + ] } }, "main": "./build/index.js", @@ -70,6 +76,7 @@ ], "types": "./build/index.d.ts", "dependencies": { + "@colors/colors": "^1.6.0", "appium-idb": "^1.6.13", "appium-ios-device": "^2.5.4", "appium-ios-simulator": "^5.5.1", @@ -81,7 +88,7 @@ "bluebird": "^3.7.2", "css-selector-parser": "^3.0.0", "fancy-log": "^2.0.0", - "js2xmlparser2": "^0.2.0", + "js2xmlparser2": "^0.x", "lodash": "^4.17.21", "lru-cache": "^10.0.0", "moment": "^2.29.4", @@ -130,7 +137,7 @@ "singleQuote": true }, "peerDependencies": { - "appium": "^2.0.0" + "appium": "^2.4.1" }, "devDependencies": { "@appium/docutils": "^0.x",