From c2566146fab45a9057eb10eedc91c9b8a7597126 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sat, 10 Feb 2024 09:25:48 +0100 Subject: [PATCH] feat: Add doctor checks (#97) --- README.md | 21 +++--- lib/doctor/optional-checks.js | 123 ++++++++++++++++++++++++++++++++++ lib/doctor/required-checks.js | 34 ++++++++++ lib/doctor/utils.js | 17 +++++ package.json | 23 +++---- 5 files changed, 196 insertions(+), 22 deletions(-) create mode 100644 lib/doctor/optional-checks.js create mode 100644 lib/doctor/required-checks.js create mode 100644 lib/doctor/utils.js diff --git a/README.md b/README.md index ed62569..fb7753a 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,12 @@ Under the hood this driver is a wrapper/proxy over `geckodriver` binary. Check t > **Note** > -> Since version 1.0.0 Gecko driver has dropped the support of Appium 1, and is only compatible to Appium 2. Use the `appium driver install gecko` +> Since version 1.0.0 Gecko driver has dropped the support of Appium 1, and is only compatible to Appium 2. Use the `appium driver install gecko` > command to add it to your Appium 2 dist. +## Requirements -## Usage - -It is mandatory to have both Firefox browser installed and the geckodriver binary downloaded on the platform where automated tests are going to be executed. Firefox could be downloaded from the [official download site](https://www.mozilla.org/en-GB/firefox/all/) and the driver binary could be retrieved from the GitHub [releases page](https://github.com/mozilla/geckodriver/releases). The binary must be put into one of the folders included to PATH environment variable. On macOS it also might be necessary to run `xcattr -cr ""` to avoid [notarization](https://firefox-source-docs.mozilla.org/testing/geckodriver/Notarization.html) issues. +It is mandatory to have both Firefox browser installed and the geckodriver binary downloaded on the platform where automated tests are going to be executed. Firefox could be downloaded from the [official download site](https://www.mozilla.org/en-GB/firefox/all/) and the driver binary could be retrieved from the GitHub [releases page](https://github.com/mozilla/geckodriver/releases). The binary must be put into one of the folders included to PATH environment variable. On macOS it also might be necessary to run `xattr -cr ""` to avoid [notarization](https://firefox-source-docs.mozilla.org/testing/geckodriver/Notarization.html) issues. Then you need to decide where the automated test is going to be executed. Gecko driver supports the following target platforms: - macOS @@ -26,9 +25,17 @@ Then you need to decide where the automated test is going to be executed. Gecko - Linux - Android (note that `android` *cannot* be passed as a value to `platformName` capability; it should always equal to the *host* platform name) -In order to run your automated tests on Android it is necessary to have [Android SDK](https://developer.android.com/studio) installed, so the destination device is marked as `online` in the `adb devices -l` command output. +In order to run your automated tests on an Android device it is necessary to have [Android SDK](https://developer.android.com/studio) installed, so the destination device is marked as `online` in the `adb devices -l` command output. + +### Doctor + +Since driver version 1.3.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 gecko` server command. -Gecko driver allows to define multiple criterions for platform selection and also to fine-tune your automation session properties. This could be done via the following session capabilities: +## Capabilities + +Gecko driver allows defining of multiple criterions for platform selection and also to fine-tune your automation session properties. This could be done via the following session capabilities: Capability Name | Description --- | --- @@ -49,7 +56,6 @@ setWindowRect | See https://www.w3.org/TR/webdriver/#capabilities timeouts | See https://www.w3.org/TR/webdriver/#capabilities unhandledPromptBehavior | See https://www.w3.org/TR/webdriver/#capabilities - ## Example ```python @@ -146,7 +152,6 @@ def test_feature_status_page_filters(driver): filt.click() ``` - ## Development ```bash diff --git a/lib/doctor/optional-checks.js b/lib/doctor/optional-checks.js new file mode 100644 index 0000000..010e70f --- /dev/null +++ b/lib/doctor/optional-checks.js @@ -0,0 +1,123 @@ +/* eslint-disable require-await */ +import {system, fs, doctor} from '@appium/support'; +import {getAndroidBinaryPath, getSdkRootFromEnv} from 'appium-adb'; + +const ENVIRONMENT_VARS_TUTORIAL_URL = + 'https://github.com/appium/java-client/blob/master/docs/environment.md'; +const ANDROID_SDK_LINK1 = 'https://developer.android.com/studio#cmdline-tools'; +const ANDROID_SDK_LINK2 = 'https://developer.android.com/studio/intro/update#sdk-manager'; + +/** + * @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 { + /** + * @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.nokOptional(`${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!`; + if (system.isWindows() && varValue.includes('%')) { + errMsg += ` Consider replacing all references to other environment variables with absolute paths.`; + } + return doctor.nokOptional(errMsg); + } + + const stat = await fs.stat(varValue); + if (this.opts.expectDir && !stat.isDirectory()) { + return doctor.nokOptional( + `${this.varName} is expected to be a valid folder, got a file path instead`, + ); + } + if (this.opts.expectFile && stat.isDirectory()) { + return doctor.nokOptional( + `${this.varName} is expected to be a valid file, got a folder path instead`, + ); + } + + return doctor.okOptional(`${this.varName} is set to: ${varValue}`); + } + + async fix() { + return ( + `Make sure the environment variable ${this.varName} is properly configured for the Appium process. ` + + `Android SDK is required if you want to run your tests on an Android device. ` + + `Refer ${ENVIRONMENT_VARS_TUTORIAL_URL} for more details.` + ); + } + + hasAutofix() { + return false; + } + + isOptional() { + return true; + } +} +export const androidHomeCheck = new EnvVarAndPathCheck('ANDROID_HOME', {expectDir: true}); + +/** @satisfies {import('@appium/types').IDoctorCheck} */ +export class AndroidSdkCheck { + /** @type {import('@appium/types').AppiumLogger} */ + log; + + TOOL_NAMES = ['adb', 'emulator']; + + async diagnose() { + const listOfTools = this.TOOL_NAMES.join(', '); + const sdkRoot = getSdkRootFromEnv(); + if (!sdkRoot) { + return doctor.nokOptional(`${listOfTools} could not be found because ANDROID_HOME is NOT set!`); + } + + this.log.info(` Checking ${listOfTools}`); + const missingBinaries = []; + for (const binary of this.TOOL_NAMES) { + try { + this.log.info(` '${binary}' exists in ${await getAndroidBinaryPath(binary)}`); + } catch (e) { + missingBinaries.push(binary); + } + } + + if (missingBinaries.length > 0) { + return doctor.nokOptional(`${missingBinaries.join(', ')} could NOT be found in '${sdkRoot}'!`); + } + + return doctor.okOptional(`${listOfTools} exist in '${sdkRoot}'`); + } + + async fix() { + return ( + `Manually install Android SDK and set ANDROID_HOME enviornment variable. ` + + `Android SDK is required if you want to run your tests on an Android device. ` + + `Read ${[ANDROID_SDK_LINK1, ANDROID_SDK_LINK2].join(' and ')}.` + ); + } + + hasAutofix() { + return false; + } + + isOptional() { + return true; + } +} +export const androidSdkCheck = new AndroidSdkCheck(); diff --git a/lib/doctor/required-checks.js b/lib/doctor/required-checks.js new file mode 100644 index 0000000..d8325d6 --- /dev/null +++ b/lib/doctor/required-checks.js @@ -0,0 +1,34 @@ +/* eslint-disable require-await */ +import {resolveExecutablePath} from './utils'; +import {system, doctor} from '@appium/support'; + +const GD_DOWNLOAD_LINK = 'https://github.com/mozilla/geckodriver/releases'; +const GD_BINARY = `geckodriver${system.isWindows() ? '.exe' : ''}`; + +/** @satisfies {import('@appium/types').IDoctorCheck} */ +export class GeckodriverCheck { + async diagnose() { + const gdPath = await resolveExecutablePath(GD_BINARY); + return gdPath + ? doctor.ok(`${GD_BINARY} is installed at: ${gdPath}`) + : doctor.nok(`${GD_BINARY} cannot be found`); + } + + async fix() { + return ( + `${GD_BINARY} is required to pass W3C commands to the remote browser. ` + + `Please download the binary from ${GD_DOWNLOAD_LINK} and store it ` + + `to any folder listed in the PATH environment variable. Folders that ` + + `are currently present in PATH: ${process.env.PATH}` + ); + } + + hasAutofix() { + return false; + } + + isOptional() { + return false; + } +} +export const geckodriverCheck = new GeckodriverCheck(); diff --git a/lib/doctor/utils.js b/lib/doctor/utils.js new file mode 100644 index 0000000..bac53a9 --- /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 480e50b..a4c29aa 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,6 @@ "node": ">=14", "npm": ">=8" }, - "lint-staged": { - "*.js": [ - "eslint --fix" - ] - }, "prettier": { "bracketSpacing": false, "printWidth": 100, @@ -48,9 +43,10 @@ "lib": "lib" }, "peerDependencies": { - "appium": "^2.0.0-beta.40" + "appium": "^2.4.1" }, "dependencies": { + "appium-adb": "^12.0.3", "asyncbox": "^3.0.0", "bluebird": "^3.5.1", "lodash": "^4.17.4", @@ -65,16 +61,11 @@ "dev": "npm run build -- --watch", "lint": "eslint .", "lint:fix": "npm run lint -- --fix", - "precommit-msg": "echo 'Pre-commit checks...' && exit 0", - "precommit-lint": "lint-staged", "prepare": "npm run build", + "format": "prettier -w ./lib", "test": "mocha --exit --timeout 1m \"./test/unit/**/*-specs.js\"", "e2e-test": "mocha --exit --timeout 5m \"./test/functional/**/*-specs.js\"" }, - "pre-commit": [ - "precommit-msg", - "precommit-lint" - ], "devDependencies": { "@appium/eslint-config-appium": "^8.0.4", "@appium/eslint-config-appium-ts": "^0.x", @@ -102,14 +93,18 @@ "eslint-plugin-import": "^2.28.0", "eslint-plugin-mocha": "^10.1.0", "eslint-plugin-promise": "^6.1.1", - "lint-staged": "^15.0.2", "mocha": "^10.0.0", - "pre-commit": "^1.1.3", "rimraf": "^5.0.0", "semantic-release": "^23.0.0", "sinon": "^17.0.0", "ts-node": "^10.9.1", "typescript": "~5.2", "webdriverio": "^8.0.5" + }, + "doctor": { + "checks": [ + "./build/lib/doctor/required-checks.js", + "./build/lib/doctor/optional-checks.js" + ] } }