From 2c5501e1c728dddccfbfd89b6301159b006f7898 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 23 Aug 2023 20:05:26 +0200 Subject: [PATCH 1/3] feat: Switch babel to typescript --- .eslintrc.json | 17 ++++++++-- .github/workflows/functional-test.yml | 7 +++++ .mocharc.js | 2 +- babel.config.json | 25 --------------- lib/check-dependencies.js | 4 +-- lib/utils.js | 17 +++++----- lib/webdriveragent.js | 23 ++++++++------ lib/xcodebuild.js | 40 +++++++++++++++++++----- package.json | 45 ++++++++++++++++----------- tsconfig.json | 14 +++++++++ 10 files changed, 122 insertions(+), 72 deletions(-) delete mode 100644 babel.config.json create mode 100644 tsconfig.json diff --git a/.eslintrc.json b/.eslintrc.json index 69a602c5c..edee2a725 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,11 +1,22 @@ { - "extends": "@appium/eslint-config-appium", + "extends": ["@appium/eslint-config-appium-ts"], "overrides": [ { "files": "test/**/*.js", "rules": { - "func-names": "off" + "func-names": "off", + "@typescript-eslint/no-var-requires": "off" + } + }, + { + "files": "Scripts/**/*", + "parserOptions": {"sourceType": "script"}, + "rules": { + "@typescript-eslint/no-var-requires": "off" } } - ] + ], + "rules": { + "require-await": "error" + } } diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index 8c95517e9..e5c1ebad9 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -25,5 +25,12 @@ jobs: npm install mkdir -p ./Resources/WebDriverAgent.bundle name: Install dev dependencies + + - run: | + target_sim_id=$(xcrun simctl list devices available | grep "$DEVICE_NAME (" | cut -d "(" -f2 | cut -d ")" -f1) + open -Fn "$(xcode-select -p)/Applications/Simulator.app" + xcrun simctl bootstatus $target_sim_id -b + name: Preboot Simulator + - run: npm run e2e-test name: Run functional tests diff --git a/.mocharc.js b/.mocharc.js index 66d4a976a..40599e9df 100644 --- a/.mocharc.js +++ b/.mocharc.js @@ -1,4 +1,4 @@ module.exports = { - require: ['@babel/register'], + require: ['ts-node/register'], forbidOnly: Boolean(process.env.CI) }; diff --git a/babel.config.json b/babel.config.json deleted file mode 100644 index 048e9cf60..000000000 --- a/babel.config.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "presets": [ - [ - "@babel/preset-env", - { - "targets": { - "node": "14" - }, - "shippedProposals": true - } - ] - ], - "plugins": [ - "source-map-support", - "@babel/plugin-transform-runtime" - ], - "comments": false, - "sourceMaps": "both", - "env": { - "test": { - "retainLines": true, - "comments": true - } - } -} diff --git a/lib/check-dependencies.js b/lib/check-dependencies.js index 39b824373..60749d9ee 100644 --- a/lib/check-dependencies.js +++ b/lib/check-dependencies.js @@ -29,7 +29,7 @@ async function checkForDependencies () { async function bundleWDASim (xcodebuild, opts = {}) { if (xcodebuild && !_.isFunction(xcodebuild.retrieveDerivedDataPath)) { - xcodebuild = new XcodeBuild(); + xcodebuild = new XcodeBuild('', {}); opts = xcodebuild; } @@ -38,7 +38,7 @@ async function bundleWDASim (xcodebuild, opts = {}) { if (await fs.exists(wdaBundlePath)) { return wdaBundlePath; } - await buildWDASim(xcodebuild, opts); + await buildWDASim(); return wdaBundlePath; } diff --git a/lib/utils.js b/lib/utils.js index 71cac28ba..4a1191600 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -188,8 +188,8 @@ CODE_SIGN_IDENTITY = ${signingId} * @param {DeviceInfo} deviceInfo * @param {string} sdkVersion - The Xcode SDK version of OS. * @param {string} bootstrapPath - The folder path containing xctestrun file. - * @param {string} wdaRemotePort - The remote port WDA is listening on. - * @return {string} returns xctestrunFilePath for given device + * @param {number|string} wdaRemotePort - The remote port WDA is listening on. + * @return {Promise} returns xctestrunFilePath for given device * @throws if WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device * or WebDriverAgentRunner_iphonesimulator${sdkVersion|platformVersion}-x86_64.xctestrun for simulator is not found @bootstrapPath, * then it will throw file not found exception @@ -207,7 +207,7 @@ async function setXctestrunFile (deviceInfo, sdkVersion, bootstrapPath, wdaRemot /** * Return the WDA object which appends existing xctest runner content * @param {string} platformName - The name of the platform - * @param {string} version - The Xcode SDK version of OS. + * @param {number|string} wdaRemotePort - The remote port number * @return {object} returns a runner object which has USE_PORT */ function getAdditionalRunContent (platformName, wdaRemotePort) { @@ -228,6 +228,7 @@ function getAdditionalRunContent (platformName, wdaRemotePort) { * @param {DeviceInfo} deviceInfo * @param {string} sdkVersion - The Xcode SDK version of OS. * @param {string} bootstrapPath - The folder path containing xctestrun file. + * @returns {Promise} */ async function getXctestrunFilePath (deviceInfo, sdkVersion, bootstrapPath) { // First try the SDK path, for Xcode 10 (at least) @@ -256,9 +257,11 @@ async function getXctestrunFilePath (deviceInfo, sdkVersion, bootstrapPath) { } } - log.errorAndThrow(`If you are using 'useXctestrunFile' capability then you ` + + throw new Error( + `If you are using 'useXctestrunFile' capability then you ` + `need to have a xctestrun file (expected: ` + - `'${path.resolve(bootstrapPath, getXctestrunFileName(deviceInfo, sdkVersion))}')`); + `'${path.resolve(bootstrapPath, getXctestrunFileName(deviceInfo, sdkVersion))}')` + ); } @@ -316,7 +319,7 @@ function randomInt (low, high) { /** * Retrieves WDA upgrade timestamp * - * @return {?number} The UNIX timestamp of the package manifest. The manifest only gets modified on + * @return {Promise} The UNIX timestamp of the package manifest. The manifest only gets modified on * package upgrade. */ async function getWDAUpgradeTimestamp () { @@ -356,7 +359,7 @@ async function resetTestProcesses (udid, isSimulator) { * listening on given port, and is expected to return * either true or false to include/exclude the corresponding PID * from the resulting array. - * @returns {Array} - the list of matched process ids. + * @returns {Promise} - the list of matched process ids. */ async function getPIDsListeningOnPort (port, filteringFunc = null) { const result = []; diff --git a/lib/webdriveragent.js b/lib/webdriveragent.js index 4c52e91d6..40196b6ea 100644 --- a/lib/webdriveragent.js +++ b/lib/webdriveragent.js @@ -150,7 +150,7 @@ class WebDriverAgent { /** * Return boolean if WDA is running or not - * @return {boolean} True if WDA is running + * @return {Promise} True if WDA is running * @throws {Error} If there was invalid response code or body */ async isRunning () { @@ -183,7 +183,7 @@ class WebDriverAgent { * } * } * - * @return {?object} State Object + * @return {Promise} State Object * @throws {Error} If there was invalid response code or body */ async getStatus () { @@ -234,7 +234,7 @@ class WebDriverAgent { const box = strongbox(packageInfo.name); let boxItem = box.getItem(RECENT_MODULE_VERSION_ITEM_NAME); if (!boxItem) { - const timestampPath = path.resolve(process.env.HOME, WDA_UPGRADE_TIMESTAMP_PATH); + const timestampPath = path.resolve(process.env.HOME ?? '', WDA_UPGRADE_TIMESTAMP_PATH); if (await fs.exists(timestampPath)) { // TODO: It is probably a bit ugly to hardcode the recent version string, // TODO: hovewer it should do the job as a temporary transition trick @@ -279,6 +279,7 @@ class WebDriverAgent { `(${recentModuleVersion} < ${packageInfo.version})` ); try { + // @ts-ignore xcodebuild should be set await this.xcodebuild.cleanProject(); await boxItem.write(packageInfo.version); } catch (e) { @@ -289,7 +290,7 @@ class WebDriverAgent { /** * Launch WDA with preinstalled package without xcodebuild. * @param {string} sessionId Launch WDA and establish the session with this sessionId - * @return {?object} State Object + * @return {Promise} State Object */ async launchWithPreinstalledWDA(sessionId) { const xctestEnv = { @@ -330,7 +331,7 @@ class WebDriverAgent { * } * * @param {string} sessionId Launch WDA and establish the session with this sessionId - * @return {?object} State Object + * @return {Promise} State Object * @throws {Error} If there was invalid response code or body */ async launch (sessionId) { @@ -374,12 +375,15 @@ class WebDriverAgent { return await this.startWithIDB(); } + // @ts-ignore xcodebuild should be set await this.xcodebuild.init(this.noSessionProxy); // Start the xcodebuild process if (this.prebuildWDA) { + // @ts-ignore xcodebuild should be set await this.xcodebuild.prebuild(); } + // @ts-ignore xcodebuild should be set return await this.xcodebuild.start(); } @@ -463,7 +467,9 @@ class WebDriverAgent { } } else if (!this.args.webDriverAgentUrl) { this.log.info('Shutting down sub-processes'); + // @ts-ignore xcodebuild should be set await this.xcodebuild.quit(); + // @ts-ignore xcodebuild should be set await this.xcodebuild.reset(); } else { this.log.debug('Do not stop xcodebuild nor XCTest session ' + @@ -504,14 +510,15 @@ class WebDriverAgent { return this.started; } - set fullyStarted (started = false) { - this.started = started; + set fullyStarted (started) { + this.started = started ?? false; } async retrieveDerivedDataPath () { if (this.canSkipXcodebuild) { return; } + // @ts-ignore xcodebuild should be set return await this.xcodebuild.retrieveDerivedDataPath(); } @@ -519,8 +526,6 @@ class WebDriverAgent { * Reuse running WDA if it has the same bundle id with updatedWDABundleId. * Or reuse it if it has the default id without updatedWDABundleId. * Uninstall it if the method faces an exception for the above situation. - * - * @param {string} updatedWDABundleId BundleId you'd like to use */ async setupCaching () { const status = await this.getStatus(); diff --git a/lib/xcodebuild.js b/lib/xcodebuild.js index df5cac008..b46bfdbbd 100644 --- a/lib/xcodebuild.js +++ b/lib/xcodebuild.js @@ -11,6 +11,7 @@ import _ from 'lodash'; import path from 'path'; import { EOL } from 'os'; import { WDA_RUNNER_BUNDLE_ID } from './constants'; +import readline from 'node:readline'; const DEFAULT_SIGNING_ID = 'iPhone Developer'; @@ -33,6 +34,12 @@ const xcodeLog = logger.getLogger('Xcode'); class XcodeBuild { + /** + * @param {string} xcodeVersion + * @param {any} device + * @param {any} args + * @param {import('@appium/types').AppiumLogger?} log + */ constructor (xcodeVersion, device, args = {}, log = null) { this.xcodeVersion = xcodeVersion; @@ -201,7 +208,7 @@ class XcodeBuild { args.push('-resultBundleVersion', this.resultBundleVersion); } - if (this.useXctestrunFile) { + if (this.useXctestrunFile && this.xctestrunFilePath) { args.push('-xctestrun', this.xctestrunFilePath); } else { const runnerScheme = isTvOS(this.platformName) ? RUNNER_SCHEME_TV : RUNNER_SCHEME_IOS; @@ -252,6 +259,7 @@ class XcodeBuild { const {cmd, args} = this.getCommand(buildOnly); this.log.debug(`Beginning ${buildOnly ? 'build' : 'test'} with command '${cmd} ${args.join(' ')}' ` + `in directory '${this.bootstrapPath}'`); + /** @type {Record} */ const env = Object.assign({}, process.env, { USE_PORT: this.wdaRemotePort, WDA_PRODUCT_BUNDLE_IDENTIFIER: this.updatedWDABundleId || WDA_RUNNER_BUNDLE_ID, @@ -260,7 +268,7 @@ class XcodeBuild { // https://github.com/appium/WebDriverAgent/pull/105 env.MJPEG_SERVER_PORT = this.mjpegServerPort; } - const upgradeTimestamp = await getWDAUpgradeTimestamp(this.bootstrapPath); + const upgradeTimestamp = await getWDAUpgradeTimestamp(); if (upgradeTimestamp) { env.UPGRADE_TIMESTAMP = upgradeTimestamp; } @@ -283,7 +291,9 @@ class XcodeBuild { if (out.includes('Writing diagnostic log for test session to')) { // pull out the first line that begins with the path separator // which *should* be the line indicating the log file generated + // @ts-ignore logLocation is a custom property xcodebuild.logLocation = _.first(_.remove(out.trim().split('\n'), (v) => v.startsWith(path.sep))); + // @ts-ignore logLocation is a custom property xcodeLog.debug(`Log file for xcodebuild test: ${xcodebuild.logLocation}`); } @@ -295,6 +305,7 @@ class XcodeBuild { logXcodeOutput = true; // terrible hack to handle case where xcode return 0 but is failing + // @ts-ignore _wda_error_occurred is a custom property xcodebuild._wda_error_occurred = true; } @@ -303,6 +314,7 @@ class XcodeBuild { for (const line of out.split(EOL)) { xcodeLog.error(line); if (line) { + // @ts-ignore _wda_error_message is a custom property xcodebuild._wda_error_message += `${EOL}${line}`; } } @@ -315,28 +327,39 @@ class XcodeBuild { async start (buildOnly = false) { this.xcodebuild = await this.createSubProcess(buildOnly); // Store xcodebuild message + // @ts-ignore _wda_error_message is a custom property this.xcodebuild._wda_error_message = ''; // wrap the start procedure in a promise so that we can catch, and report, // any startup errors that are thrown as events return await new B((resolve, reject) => { + // @ts-ignore xcodebuild must be present here this.xcodebuild.on('exit', async (code, signal) => { xcodeLog.error(`xcodebuild exited with code '${code}' and signal '${signal}'`); // print out the xcodebuild file if users have asked for it - if (this.showXcodeLog && this.xcodebuild.logLocation) { + // @ts-ignore logLocation is a custom property + if (this.showXcodeLog && this.xcodebuild?.logLocation) { + // @ts-ignore logLocation is a custom property xcodeLog.error(`Contents of xcodebuild log file '${this.xcodebuild.logLocation}':`); try { - let data = await fs.readFile(this.xcodebuild.logLocation, 'utf8'); - for (let line of data.split('\n')) { + const logFile = readline.createInterface({ + // @ts-ignore logLocation is a custom property + input: fs.createReadStream(this.xcodebuild.logLocation), + terminal: false + }); + logFile.on('line', (line) => { xcodeLog.error(line); - } + }); } catch (err) { xcodeLog.error(`Unable to access xcodebuild log file: '${err.message}'`); } } + // @ts-ignore processExited is a custom property this.xcodebuild.processExited = true; + // @ts-ignore _wda_error_occurred is a custom property if (this.xcodebuild._wda_error_occurred || (!signal && code !== 0)) { return reject(new Error(`xcodebuild failed with code ${code}${EOL}` + + // @ts-ignore _wda_error_message is a custom property `xcodebuild error message:${EOL}${this.xcodebuild._wda_error_message}`)); } // in the case of just building, the process will exit and that is our finish @@ -348,6 +371,7 @@ class XcodeBuild { return (async () => { try { const timer = new timing.Timer().start(); + // @ts-ignore this.xcodebuild must be defined await this.xcodebuild.start(true); if (!buildOnly) { let status = await this.waitForStart(timer); @@ -367,8 +391,9 @@ class XcodeBuild { this.log.debug(`Waiting up to ${this.launchTimeout}ms for WebDriverAgent to start`); let currentStatus = null; try { - let retries = parseInt(this.launchTimeout / 500, 10); + let retries = parseInt(`${this.launchTimeout / 500}`, 10); await retryInterval(retries, 1000, async () => { + // @ts-ignore processExited is a custom property if (this.xcodebuild.processExited) { // there has been an error elsewhere and we need to short-circuit return; @@ -389,6 +414,7 @@ class XcodeBuild { } }); + // @ts-ignore processExited is a custom property if (this.xcodebuild.processExited) { // there has been an error elsewhere and we need to short-circuit return currentStatus; diff --git a/package.json b/package.json index f3cee7cd7..ba7fa2e37 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,9 @@ "description": "Package bundling WebDriverAgent", "main": "./build/index.js", "scripts": { - "build": "rm -rf build && babel --out-dir=build/lib lib && babel --out-dir=build index.js", + "build": "tsc -b", "dev": "npm run build -- --watch", + "clean": "npm run build -- --clean", "lint": "eslint .", "lint:fix": "npm run lint -- --fix", "precommit-msg": "echo 'Pre-commit checks...' && exit 0", @@ -54,43 +55,51 @@ }, "homepage": "https://github.com/appium/WebDriverAgent#readme", "devDependencies": { - "@appium/eslint-config-appium": "^6.0.0", - "@appium/test-support": "^3.0.0", - "@babel/cli": "^7.18.10", - "@babel/core": "^7.18.10", - "@babel/eslint-parser": "^7.18.9", - "@babel/plugin-transform-runtime": "^7.18.10", - "@babel/preset-env": "^7.18.10", - "@babel/register": "^7.18.9", + "@appium/eslint-config-appium": "^8.0.4", + "@appium/eslint-config-appium-ts": "^0.3.1", + "@appium/tsconfig": "^0.3.0", + "@appium/types": "^0.13.2", "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", - "babel-plugin-source-map-support": "^2.2.0", + "@types/bluebird": "^3.5.38", + "@types/chai": "^4.3.5", + "@types/chai-as-promised": "^7.1.5", + "@types/lodash": "^4.14.196", + "@types/mocha": "^10.0.1", + "@types/node": "^20.4.7", + "@types/sinon": "^10.0.16", + "@types/sinon-chai": "^3.2.9", + "@types/teen_process": "2.0.0", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", "appium-xcode": "^5.0.0", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", "conventional-changelog-conventionalcommits": "^6.0.0", - "eslint": "^7.32.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-mocha": "^9.0.0", - "eslint-plugin-promise": "^6.0.0", + "eslint": "^8.46.0", + "eslint-config-prettier": "^8.9.0", + "eslint-import-resolver-typescript": "^3.5.5", + "eslint-plugin-import": "^2.28.0", + "eslint-plugin-mocha": "^10.1.0", + "eslint-plugin-promise": "^6.1.1", "lint-staged": "^14.0.0", "mocha": "^10.0.0", "pre-commit": "^1.2.2", "prettier": "^3.0.0", "semantic-release": "^20.0.2", - "sinon": "^15.0.0" + "sinon": "^15.0.0", + "ts-node": "^10.9.1", + "typescript": "^5.1.6" }, "dependencies": { "@appium/base-driver": "^9.0.0", "@appium/strongbox": "^0.x", "@appium/support": "^4.0.0", - "@babel/runtime": "^7.0.0", "appium-ios-device": "^2.5.0", "appium-ios-simulator": "^5.0.1", "async-lock": "^1.0.0", "asyncbox": "^2.5.3", - "axios": "^1.x", + "axios": "^1.4.0", "bluebird": "^3.5.5", "lodash": "^4.17.11", "node-simctl": "^7.0.1", diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..33d83ef45 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@appium/tsconfig/tsconfig.json", + "compilerOptions": { + "strict": false, // TODO: make this flag true + "outDir": "build", + "types": ["node"], + "checkJs": true + }, + "include": [ + "index.js", + "lib" + ] +} From 322f50109e6afd220678764d46bc1e8af2a98177 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 23 Aug 2023 20:12:11 +0200 Subject: [PATCH 2/3] Add await --- lib/xcodebuild.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/xcodebuild.js b/lib/xcodebuild.js index b46bfdbbd..1e59ec7e6 100644 --- a/lib/xcodebuild.js +++ b/lib/xcodebuild.js @@ -350,6 +350,12 @@ class XcodeBuild { logFile.on('line', (line) => { xcodeLog.error(line); }); + await new B((resolve) => { + logFile.once('close', () => { + logFile.removeAllListeners(); + resolve(); + }); + }); } catch (err) { xcodeLog.error(`Unable to access xcodebuild log file: '${err.message}'`); } From d3388a83316355fb824dc304c28b261861a56758 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Wed, 23 Aug 2023 21:21:43 +0200 Subject: [PATCH 3/3] Update lint rules --- .eslintrc.json | 9 ++++++++- lib/check-dependencies.js | 3 +-- lib/xcodebuild.js | 4 ++-- package.json | 1 + 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index edee2a725..3d7db0846 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -9,7 +9,14 @@ } }, { - "files": "Scripts/**/*", + "files": "Scripts/**/*.js", + "parserOptions": {"sourceType": "script"}, + "rules": { + "@typescript-eslint/no-var-requires": "off" + } + }, + { + "files": "ci-jobs/scripts/*.js", "parserOptions": {"sourceType": "script"}, "rules": { "@typescript-eslint/no-var-requires": "off" diff --git a/lib/check-dependencies.js b/lib/check-dependencies.js index 60749d9ee..ce145f5a1 100644 --- a/lib/check-dependencies.js +++ b/lib/check-dependencies.js @@ -27,10 +27,9 @@ async function checkForDependencies () { return false; } -async function bundleWDASim (xcodebuild, opts = {}) { +async function bundleWDASim (xcodebuild) { if (xcodebuild && !_.isFunction(xcodebuild.retrieveDerivedDataPath)) { xcodebuild = new XcodeBuild('', {}); - opts = xcodebuild; } const derivedDataPath = await xcodebuild.retrieveDerivedDataPath(); diff --git a/lib/xcodebuild.js b/lib/xcodebuild.js index 1e59ec7e6..348a4f967 100644 --- a/lib/xcodebuild.js +++ b/lib/xcodebuild.js @@ -350,10 +350,10 @@ class XcodeBuild { logFile.on('line', (line) => { xcodeLog.error(line); }); - await new B((resolve) => { + await new B((_resolve) => { logFile.once('close', () => { logFile.removeAllListeners(); - resolve(); + _resolve(); }); }); } catch (err) { diff --git a/package.json b/package.json index ba7fa2e37..18892b959 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "devDependencies": { "@appium/eslint-config-appium": "^8.0.4", "@appium/eslint-config-appium-ts": "^0.3.1", + "@appium/test-support": "^3.0.0", "@appium/tsconfig": "^0.3.0", "@appium/types": "^0.13.2", "@semantic-release/changelog": "^6.0.1",