From c173dec310b97cf4663ed0932abfe138e16ef437 Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Mon, 10 Jun 2019 14:54:39 +0300 Subject: [PATCH 01/51] chore: bump version to 5.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4855451..4af37a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nativescript-dev-appium", - "version": "5.3.0", + "version": "5.4.0", "description": "A NativeScript plugin to help integrate and run Appium tests", "author": "NativeScript", "license": "MIT", From ae4556f26b1bbb6c467a91b65b660a0776059d2e Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Wed, 12 Jun 2019 17:44:11 +0300 Subject: [PATCH 02/51] doc: remove devMode option --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 8c481f4..a880758 100644 --- a/README.md +++ b/README.md @@ -266,7 +266,6 @@ As you can see, the `app` property can be left an empty string which will force |---|---|---| |runType| Select the capabilities from your config file `appium.capabilities.json`| Consider using `android`, `device`, `sim` strings as part of your `runType` option if you haven't provided `app` capability. Thus, the runner will look for app package in the right location for the current run.
e.g. --runType ios-device10iPhone6| |appPath| Provide location of the app package to be tested. This will overwrite all provided capabilities for app| Possible values are:
- app build package name (in case `--sauceLab` option is set it will prepend `sauce-storage:` in front of the app name so app has to be [uploaded to Sauce Labs](https://wiki.saucelabs.com/display/DOCS/Uploading+Mobile+Applications+to+Sauce+Storage+for+Testing) before execution starts)
- path e.g. `platforms/android/build/outputs/apk/demo.apk`.
Example: --appPath demo-debug.apk| -| devMode | `devMode` capabilities. Skipping application installation and will automatically reuse device. | e.g. --devMode | |sauceLab| Enable tests execution in [Sauce Labs](https://saucelabs.com/). As a prerequisite you will have to define `SAUCE_USER` and `SAUCE_KEY` as [environment variable](https://wiki.saucelabs.com/display/DOCS/Best+Practice%3A+Use+Environment+Variables+for+Authentication+Credentials)| e.g. --sauceLab| |appiumCapsLocation| Change the location where `appium.capabilities.json` config file can be. It should be relative to the root directory | e.g. --appiumCapsLocation /e2e-tests| |port| Appium server port| From 8fec7e27d90bced0200374e5a1cdd019313b0c42 Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Wed, 12 Jun 2019 17:55:37 +0300 Subject: [PATCH 03/51] doc: update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a880758..7560cfc 100644 --- a/README.md +++ b/README.md @@ -254,7 +254,7 @@ Notice that once custom capabilities are provided you will be able to pick any o } ``` -As you can see, the `app` property can be left an empty string which will force the plugin to search for an app package in `platforms` folder. However, this search functionality depends on `runType` option so if you think of using it add `android`, `device`, `sim` strings as part of your `runType` option which in fact is your capability key in the config file. E.g --runType android23, --runType sim.10iPhone6. Thus, the runner will manage to look in the right location in order to search for app package. +As you can see, the `app` property can be left an empty string which will force the plugin to search for an app package in `platforms` folder. However, this search functionality depends on `runType` option so if you think of using it add `android`, `device`, `sim` strings as part of your `runType` option which in fact is your capability key in the config file. E.g --runType android23, --runType sim.iPhone8.iOS110. Thus, the runner will manage to look in the right location in order to search for app package. **It is important to build your app in advance as explained in [Usage](#usage) section, because the runner expects to provide app package to it or such to exists in the search location.** From 6c2735cad8e55a688ffb11182111cdbab2185b84 Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Sun, 23 Jun 2019 23:48:23 +0300 Subject: [PATCH 04/51] refactor(compare): add waitOnCreatingInitialSnapshot (#231) --- lib/appium-driver.ts | 3 +++ lib/image-helper.d.ts | 2 ++ lib/image-helper.ts | 13 ++++++++++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index 5330b84..4c4ecdc 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -590,6 +590,9 @@ export class AppiumDriver { // First time capture if (!existsSync(pathExpectedImage)) { const pathActualImage = resolvePath(this._storageByDeviceName, imageName.replace(".", "_actual.")); + if (this.imageHelper.waitOnCreatingInitialSnapshot > 0) { + await this.driver.wait(this.imageHelper.waitOnCreatingInitialSnapshot); + } await this.takeScreenshot(pathActualImage); if (rect) { diff --git a/lib/image-helper.d.ts b/lib/image-helper.d.ts index 5b2d5df..1495a81 100644 --- a/lib/image-helper.d.ts +++ b/lib/image-helper.d.ts @@ -5,7 +5,9 @@ export declare class ImageHelper { private _args; private _imageCropRect; private _blockOutAreas; + private _waitOnCreatingInitialSnapshot; constructor(_args: INsCapabilities); + waitOnCreatingInitialSnapshot: number; imageCropRect: IRectangle; blockOutAreas: IRectangle[]; imageOutputLimit(): ImageOptions; diff --git a/lib/image-helper.ts b/lib/image-helper.ts index e9145df..27d63aa 100644 --- a/lib/image-helper.ts +++ b/lib/image-helper.ts @@ -10,10 +10,19 @@ export class ImageHelper { private _imageCropRect: IRectangle; private _blockOutAreas: IRectangle[]; + private _waitOnCreatingInitialSnapshot: number; constructor(private _args: INsCapabilities) { } + get waitOnCreatingInitialSnapshot() { + return this._waitOnCreatingInitialSnapshot; + } + + set waitOnCreatingInitialSnapshot(waitOnCreatingInitialSnapshot: number) { + this._waitOnCreatingInitialSnapshot = waitOnCreatingInitialSnapshot; + } + get imageCropRect(): IRectangle { return this._imageCropRect; } @@ -103,9 +112,11 @@ export class ImageHelper { if (typeThreshold == ImageOptions.percent) { valueThreshold = Math.floor(valueThreshold * 100); + console.log(`Using ${valueThreshold}\ ${typeThreshold} tolerance`); + } else { + console.log(`Using ${valueThreshold} tolerance`); } - console.log(`Using ${valueThreshold}\ ${typeThreshold} tolerance`); const result = this.runDiff(diff, output); this._blockOutAreas = undefined; return result; From 65c6aa37d4b099b39999fe0aa347e32c547b1799 Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Tue, 25 Jun 2019 01:13:41 +0300 Subject: [PATCH 05/51] fix: typos --- lib/appium-driver.ts | 2 +- lib/enums/log-image-type.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index 4c4ecdc..d55507c 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -591,7 +591,7 @@ export class AppiumDriver { if (!existsSync(pathExpectedImage)) { const pathActualImage = resolvePath(this._storageByDeviceName, imageName.replace(".", "_actual.")); if (this.imageHelper.waitOnCreatingInitialSnapshot > 0) { - await this.driver.wait(this.imageHelper.waitOnCreatingInitialSnapshot); + await this.wait(this.imageHelper.waitOnCreatingInitialSnapshot); } await this.takeScreenshot(pathActualImage); diff --git a/lib/enums/log-image-type.ts b/lib/enums/log-image-type.ts index 11c6111..ad46f7c 100644 --- a/lib/enums/log-image-type.ts +++ b/lib/enums/log-image-type.ts @@ -1,13 +1,13 @@ export enum LogImageType { /** - * Setting this property to add each image + * Set this property to add each image * during the image comparison into the report. * If not set, it will be logged only the last image comparison. */ everyImage = "everyImage", /** - * Setting this property to take screenshot on each hook + * Set this property to take screenshot on each hook * and add the images into the report. */ - screenshots = "screenshots" + screenshots = "screenshots", } \ No newline at end of file From ef270c952568562dc1418eba72570f3c727d6757 Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Wed, 26 Jun 2019 20:11:27 +0300 Subject: [PATCH 06/51] feat: resolve symlinked storages (#235) --- lib/appium-driver.ts | 3 +-- lib/enums/log-image-type.d.ts | 4 ++-- lib/utils.ts | 11 ++++++++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index d55507c..5440ba6 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -239,7 +239,7 @@ export class AppiumDriver { prepareApp(args); if (!args.device) { if (args.isAndroid) { - args.device = DeviceManager.getDefaultDevice(args, sessionInfo.capabilities.avd, sessionInfo.capabilities.deviceUDID.replace("emulator-", ""), sessionInfo.capabilities.deviceUDID.includes("emulator") ? DeviceType.EMULATOR : DeviceType.SIMULATOR, sessionInfo.capabilities.desired.platformVersion || sessionInfo.capabilities.platformVersion); + args.device = DeviceManager.getDefaultDevice(args, sessionInfo.capabilities.deviceName, sessionInfo.capabilities.deviceUDID.replace("emulator-", ""), sessionInfo.capabilities.deviceUDID.includes("emulator") ? DeviceType.EMULATOR : DeviceType.SIMULATOR, sessionInfo.capabilities.desired.platformVersion || sessionInfo.capabilities.platformVersion); } else { args.device = DeviceManager.getDefaultDevice(args); } @@ -605,7 +605,6 @@ export class AppiumDriver { console.log("Remove the 'actual' suffix to continue using the image as expected one ", pathExpectedImage); this._args.testReporterLog(basename(pathActualImage).replace(/\.\w{3,3}$/ig, "")); this._args.testReporterLog(join(this._logPath, basename(pathActualImage))); - return false; } diff --git a/lib/enums/log-image-type.d.ts b/lib/enums/log-image-type.d.ts index c022605..04966e8 100644 --- a/lib/enums/log-image-type.d.ts +++ b/lib/enums/log-image-type.d.ts @@ -1,12 +1,12 @@ export declare enum LogImageType { /** - * Setting this property to add each image + * Set this property to add each image * during the image comparison into the report. * If not set, it will be logged only the last image comparison. */ everyImage = "everyImage", /** - * Setting this property to take screenshot on each hook + * Set this property to take screenshot on each hook * and add the images into the report. */ screenshots = "screenshots" diff --git a/lib/utils.ts b/lib/utils.ts index c8defec..471ed5a 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -13,7 +13,10 @@ import { mkdirSync, readdirSync, writeFileSync, - readFileSync + readFileSync, + readlinkSync, + lstatSync, + realpathSync } from "fs"; import { extname, @@ -440,6 +443,12 @@ export async function scroll(wd, driver, direction: Direction, isIOS: boolean, y function createStorageFolder(storage, directory) { storage = resolvePath(storage, directory); + try { + storage = readlinkSync(storage); + } catch (error) { } + try { + storage = realpathSync(storage); + } catch (error) { } if (!existsSync(storage)) { mkdirSync(storage); } From 0d2c9fd07c34222915bd0400fe25b96154efbb5c Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Thu, 27 Jun 2019 02:18:53 +0300 Subject: [PATCH 07/51] feat: image-helper (#236) --- index.d.ts | 1 + index.ts | 9 +-- lib/appium-driver.d.ts | 2 +- lib/appium-driver.ts | 12 ++-- lib/image-helper.d.ts | 36 ++++++++++-- lib/image-helper.ts | 124 ++++++++++++++++++++++++++++++++++++++--- 6 files changed, 162 insertions(+), 22 deletions(-) diff --git a/index.d.ts b/index.d.ts index 9034ecc..912b643 100644 --- a/index.d.ts +++ b/index.d.ts @@ -22,6 +22,7 @@ export { logInfo, logError, logWarn } from "./lib/utils"; export { ITestReporter } from "./lib/interfaces/test-reporter"; export { screencapture } from "./lib/helpers/screenshot-manager"; export { LogImageType } from "./lib/enums/log-image-type"; +export { ImageHelper, IImageCompareOptions } from "./lib/image-helper"; export declare const nsCapabilities: INsCapabilities; export declare function startServer(port?: number, deviceManager?: IDeviceManager): Promise; export declare function stopServer(): Promise; diff --git a/index.ts b/index.ts index eb76dbe..a862caa 100644 --- a/index.ts +++ b/index.ts @@ -32,6 +32,7 @@ export { logInfo, logError, logWarn } from "./lib/utils"; export { ITestReporter } from "./lib/interfaces/test-reporter"; export { screencapture } from "./lib/helpers/screenshot-manager"; export { LogImageType } from "./lib/enums/log-image-type"; +export { ImageHelper, IImageCompareOptions } from "./lib/image-helper"; export const nsCapabilities: INsCapabilities = new NsCapabilities(parser); @@ -139,7 +140,7 @@ const killProcesses = async (code) => { process.removeAllListeners(); try { //if (isWin() && process) { - // process.exit(0); + // process.exit(0); //} } catch (error) { } } @@ -152,9 +153,9 @@ const attachToExitProcessHookup = (processToAttach, processName) => { } signals.forEach(function (sig) { processToAttach.once(sig, async function () { - await killProcesses(sig); - console.log(`Exited from ${processName}`); - processToAttach.removeListener(sig, killProcesses); + await killProcesses(sig); + console.log(`Exited from ${processName}`); + processToAttach.removeListener(sig, killProcesses); }); }); } \ No newline at end of file diff --git a/lib/appium-driver.d.ts b/lib/appium-driver.d.ts index fb76004..35cf83a 100644 --- a/lib/appium-driver.d.ts +++ b/lib/appium-driver.d.ts @@ -40,7 +40,7 @@ export declare class AppiumDriver { readonly isIOS: boolean; readonly driver: any; /** - * Get the storage where test results from image comparisson is logged It will be reports/app nam/device name + * Get the storage where test results from image comparison is logged It will be reports/app nam/device name */ readonly reportsPath: string; /** diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index 5440ba6..1790c87 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -67,7 +67,7 @@ export class AppiumDriver { private constructor(private _driver: any, private _wd, private _webio: any, private _driverConfig, private _args: INsCapabilities) { this._elementHelper = new ElementHelper(this._args); - this._imageHelper = new ImageHelper(this._args); + this._imageHelper = new ImageHelper(this._args, this); this._isAlive = true; this._locators = new Locator(this._args); this._webio.requestHandler.sessionID = this._driver.sessionID; @@ -126,7 +126,7 @@ export class AppiumDriver { } /** - * Get the storage where test results from image comparisson is logged It will be reports/app nam/device name + * Get the storage where test results from image comparison is logged It will be reports/app nam/device name */ get reportsPath() { return this._logPath; @@ -239,7 +239,7 @@ export class AppiumDriver { prepareApp(args); if (!args.device) { if (args.isAndroid) { - args.device = DeviceManager.getDefaultDevice(args, sessionInfo.capabilities.deviceName, sessionInfo.capabilities.deviceUDID.replace("emulator-", ""), sessionInfo.capabilities.deviceUDID.includes("emulator") ? DeviceType.EMULATOR : DeviceType.SIMULATOR, sessionInfo.capabilities.desired.platformVersion || sessionInfo.capabilities.platformVersion); + args.device = DeviceManager.getDefaultDevice(args, sessionInfo.capabilities.desired.deviceName, sessionInfo.capabilities.deviceUDID.replace("emulator-", ""), sessionInfo.capabilities.deviceUDID.includes("emulator") ? DeviceType.EMULATOR : DeviceType.SIMULATOR, sessionInfo.capabilities.desired.platformVersion || sessionInfo.capabilities.platformVersion); } else { args.device = DeviceManager.getDefaultDevice(args); } @@ -589,9 +589,9 @@ export class AppiumDriver { // First time capture if (!existsSync(pathExpectedImage)) { - const pathActualImage = resolvePath(this._storageByDeviceName, imageName.replace(".", "_actual.")); - if (this.imageHelper.waitOnCreatingInitialSnapshot > 0) { - await this.wait(this.imageHelper.waitOnCreatingInitialSnapshot); + const pathActualImage = resolvePath(this._storageByDeviceName, this.imageHelper.options.preserveImageName ? imageName : imageName.replace(".", "_actual.")); + if (this.imageHelper.options.waitOnCreatingInitialSnapshot > 0) { + await this.wait(this.imageHelper.options.waitOnCreatingInitialSnapshot); } await this.takeScreenshot(pathActualImage); diff --git a/lib/image-helper.d.ts b/lib/image-helper.d.ts index 1495a81..bcbe9d6 100644 --- a/lib/image-helper.d.ts +++ b/lib/image-helper.d.ts @@ -1,18 +1,46 @@ import { ImageOptions } from "./image-options"; import { INsCapabilities } from "./interfaces/ns-capabilities"; import { IRectangle } from "./interfaces/rectangle"; +import { UIElement } from "./ui-element"; +import { AppiumDriver } from "./appium-driver"; +export interface IImageCompareOptions { + imageName?: string; + timeOutSeconds?: number; + tolerance?: number; + toleranceType?: ImageOptions; + /** + * wait miliseconds before capture creating image + */ + waitOnCreatingInitialSnapshot?: number; + /** + * This property will keep image name as it is and will not add _actual postfix on initial capture + */ + preserveImageName?: boolean; +} export declare class ImageHelper { private _args; + private _driver; private _imageCropRect; private _blockOutAreas; - private _waitOnCreatingInitialSnapshot; - constructor(_args: INsCapabilities); - waitOnCreatingInitialSnapshot: number; + private _imagesResults; + private _testName; + private _options; + constructor(_args: INsCapabilities, _driver: AppiumDriver); + options: IImageCompareOptions; + testName: string; + imageComppareOptions: IImageCompareOptions; + compareScreen(options?: IImageCompareOptions): Promise; + compareElement(element: UIElement, options?: IImageCompareOptions): Promise; + compareRectangle(element: IRectangle, options?: IImageCompareOptions): Promise; + hasImageComparisonPassed(): boolean; + reset(): void; + private increaseImageName; + private extendOptions; imageCropRect: IRectangle; blockOutAreas: IRectangle[]; imageOutputLimit(): ImageOptions; thresholdType(): ImageOptions; - threshold(thresholdType: any): 10 | 0.01; + threshold(thresholdType: any): 0.01 | 10; delta(): number; static cropImageDefault(_args: INsCapabilities): { x: number; diff --git a/lib/image-helper.ts b/lib/image-helper.ts index 27d63aa..bad7907 100644 --- a/lib/image-helper.ts +++ b/lib/image-helper.ts @@ -1,26 +1,136 @@ +import { basename } from "path"; import * as BlinkDiff from "blink-diff"; import * as PngJsImage from "pngjs-image"; import { ImageOptions } from "./image-options"; import { INsCapabilities } from "./interfaces/ns-capabilities"; import { IRectangle } from "./interfaces/rectangle"; import { LogImageType } from "./enums/log-image-type"; -import { basename } from "path"; +import { UIElement } from "./ui-element"; +import { AppiumDriver } from "./appium-driver"; +import { logError } from "./utils"; + +export interface IImageCompareOptions { + imageName?: string; + timeOutSeconds?: number; + tolerance?: number; + toleranceType?: ImageOptions; + /** + * wait miliseconds before capture creating image + */ + waitOnCreatingInitialSnapshot?: number; + /** + * This property will keep image name as it is and will not add _actual postfix on initial capture + */ + preserveImageName?: boolean; +} export class ImageHelper { private _imageCropRect: IRectangle; private _blockOutAreas: IRectangle[]; - private _waitOnCreatingInitialSnapshot: number; + private _imagesResults = new Map(); + private _testName: string; + private _options: IImageCompareOptions = { + timeOutSeconds: 2, + tolerance: 0, + toleranceType: ImageOptions.pixel, + waitOnCreatingInitialSnapshot: 2000, + preserveImageName: false, + }; + + constructor(private _args: INsCapabilities, private _driver: AppiumDriver) { + } + + get options() { + return this._options; + } + + set options(options: IImageCompareOptions) { + this._options = this.extendOptions(options); + } - constructor(private _args: INsCapabilities) { + set testName(testName: string) { + this._testName = testName; } - get waitOnCreatingInitialSnapshot() { - return this._waitOnCreatingInitialSnapshot; + get testName() { + return this._testName; } - set waitOnCreatingInitialSnapshot(waitOnCreatingInitialSnapshot: number) { - this._waitOnCreatingInitialSnapshot = waitOnCreatingInitialSnapshot; + get imageComppareOptions() { + this.extendOptions(this._options); + + return this._options; + } + + set imageComppareOptions(imageComppareOptions: IImageCompareOptions) { + this._options = this.extendOptions(imageComppareOptions); + } + + public async compareScreen(options?: IImageCompareOptions) { + options = this.extendOptions(options); + const imageName = this.increaseImageName(options.imageName || this._testName); + const result = await this._driver.compareScreen(imageName, options.timeOutSeconds, options.tolerance, options.toleranceType); + this._imagesResults.set(imageName, result); + + return result; + } + + public async compareElement(element: UIElement, options?: IImageCompareOptions) { + options = this.extendOptions(options); + const imageName = this.increaseImageName(options.imageName || this._testName); + const result = await this._driver.compareElement(element, imageName, options.tolerance, options.timeOutSeconds, options.toleranceType); + this._imagesResults.set(imageName, result); + + return result; + } + + public async compareRectangle(element: IRectangle, options?: IImageCompareOptions) { + options = this.extendOptions(options); + const imageName = this.increaseImageName(options.imageName || this._testName); + const result = await this._driver.compareRectangle(element, imageName, options.timeOutSeconds, options.tolerance, options.toleranceType); + this._imagesResults.set(imageName, result); + + return result; + } + + public hasImageComparisonPassed() { + let shouldFailTest = true; + console.log(); + this._imagesResults.forEach((v, k, map) => { + if (!this._imagesResults.get(k)) { + shouldFailTest = false; + this._driver.testReporterLog(`Image comparison for image ${k} has failed!`); + logError(`Image comparison for image ${k} has failed`); + } + }); + + this.reset(); + return shouldFailTest; + } + + public reset() { + this._imagesResults.clear(); + } + + private increaseImageName(imageName: string) { + if (this._imagesResults.size > 1) { + const number = /\d+$/.test(imageName) ? +`${/\d+$/.exec(imageName)}` + 1 : `2`; + imageName = `${imageName}_${number}`; + } + + return imageName; + } + + private extendOptions(options: IImageCompareOptions) { + options = options || {}; + Object.getOwnPropertyNames(this.options).forEach(prop => { + if (!options[prop]) { + options[prop] = this.options[prop]; + } + }); + + return options; } get imageCropRect(): IRectangle { From c075af81befe5ac12fb8fe28001ca3953881b6ca Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Fri, 28 Jun 2019 22:00:24 +0300 Subject: [PATCH 08/51] fix(image-helper): increase image name (#237) --- lib/image-helper.ts | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/image-helper.ts b/lib/image-helper.ts index bad7907..37f40f2 100644 --- a/lib/image-helper.ts +++ b/lib/image-helper.ts @@ -111,12 +111,37 @@ export class ImageHelper { public reset() { this._imagesResults.clear(); + this.testName = undefined; } private increaseImageName(imageName: string) { - if (this._imagesResults.size > 1) { - const number = /\d+$/.test(imageName) ? +`${/\d+$/.exec(imageName)}` + 1 : `2`; - imageName = `${imageName}_${number}`; + if (!imageName) { + logError(`Missing image name!`); + logError(`Consider to set + drive.imageHelper.testName + dirver.imageHelper.options.imageName + `); + throw new Error(`Missing image name!`) + } + if (this._imagesResults.size > 0) { + const images = new Array(); + this._imagesResults.forEach((v, k, map) => { + if (k.includes(imageName)) { + images.push(k); + } + }); + + images.sort((a, b) => { + const aNumber = +/\d+$/.exec(a); + const bNumber = +/\d+$/.exec(b); + + return bNumber - aNumber; + }); + if (images.length > 0) { + const lastImage = images[0]; + const number = /\d+$/.test(lastImage) ? +`${/\d+$/.exec(lastImage)}` + 1 : `2`; + imageName = `${imageName}_${number}`; + } } return imageName; From 69ad7a97d41d5b6bf9a9ab8158e755e68da10833 Mon Sep 17 00:00:00 2001 From: Tom Faltesek <10088641+tomfaltesek@users.noreply.github.com> Date: Mon, 5 Aug 2019 05:16:04 -0500 Subject: [PATCH 09/51] Add Jasmine-specific note about params to readme. (#240) Added a small note to the readme about using the automatically scaffolded `npm run e2e` script` with jasmine. This detail would have saved me some frustration. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 7560cfc..898dc5d 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,11 @@ $ npm run e2e -- --device.name=/iPhone X/ --device.apiLevel=/12.1/ Generated tests are standard [Mocha](http://mochajs.org) tests. +NOTE: When using Jasmine instead of Mocha, additional npm params (like `runType`) must have an equal sign (=) instead of a space. +``` +npm run e2e -- --runType=sim.iPhoneX +``` + ## Blogs 2018, March 6th: [Start Testing Your NativeScript Apps Properly](https://www.nativescript.org/blog/start-testing-your-nativescript-apps-properly) From e0cd6e523a79714cb12ebb57b984aa7c7fcd5a27 Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Wed, 7 Aug 2019 16:16:09 +0300 Subject: [PATCH 10/51] refactor: use 'Appium' viewportRect to define rect for image comparison (#238) --- index.d.ts | 1 + index.ts | 3 +- lib/appium-driver.d.ts | 47 ++- lib/appium-driver.ts | 370 ++++++++--------- lib/appium-server.ts | 2 +- lib/device-manager.d.ts | 2 + lib/device-manager.ts | 46 ++- lib/element-helper.d.ts | 2 +- lib/element-helper.ts | 16 +- lib/enums/device-orientation.d.ts | 4 + lib/enums/device-orientation.ts | 4 + lib/frame-comparer.ts | 3 +- lib/image-helper.d.ts | 103 +++-- lib/image-helper.ts | 487 ++++++++++++++++------- lib/interfaces/ns-capabilities-args.d.ts | 3 + lib/interfaces/ns-capabilities-args.ts | 3 + lib/interfaces/rectangle.d.ts | 8 +- lib/interfaces/rectangle.ts | 8 +- lib/ns-capabilities.d.ts | 15 +- lib/ns-capabilities.ts | 64 ++- lib/parser.d.ts | 2 +- lib/parser.ts | 9 +- lib/point.d.ts | 4 +- lib/point.ts | 18 +- lib/ui-element.d.ts | 3 + lib/ui-element.ts | 6 +- lib/utils.ts | 18 +- package.json | 6 +- 28 files changed, 811 insertions(+), 446 deletions(-) create mode 100644 lib/enums/device-orientation.d.ts create mode 100644 lib/enums/device-orientation.ts diff --git a/index.d.ts b/index.d.ts index 912b643..7d6bee0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -23,6 +23,7 @@ export { ITestReporter } from "./lib/interfaces/test-reporter"; export { screencapture } from "./lib/helpers/screenshot-manager"; export { LogImageType } from "./lib/enums/log-image-type"; export { ImageHelper, IImageCompareOptions } from "./lib/image-helper"; +export { DeviceOrientation } from "./lib/enums/device-orientation"; export declare const nsCapabilities: INsCapabilities; export declare function startServer(port?: number, deviceManager?: IDeviceManager): Promise; export declare function stopServer(): Promise; diff --git a/index.ts b/index.ts index a862caa..f964291 100644 --- a/index.ts +++ b/index.ts @@ -10,8 +10,6 @@ import { logInfo, logError } from "./lib/utils"; import { INsCapabilities } from "./lib/interfaces/ns-capabilities"; import { INsCapabilitiesArgs } from "./lib/interfaces/ns-capabilities-args"; import * as parser from "./lib/parser" -import { isWin } from "./lib/utils"; -import { LogImageType } from "./lib/enums/log-image-type"; export { AppiumDriver } from "./lib/appium-driver"; export { AppiumServer } from "./lib/appium-server"; @@ -33,6 +31,7 @@ export { ITestReporter } from "./lib/interfaces/test-reporter"; export { screencapture } from "./lib/helpers/screenshot-manager"; export { LogImageType } from "./lib/enums/log-image-type"; export { ImageHelper, IImageCompareOptions } from "./lib/image-helper"; +export { DeviceOrientation } from "./lib/enums/device-orientation"; export const nsCapabilities: INsCapabilities = new NsCapabilities(parser); diff --git a/lib/appium-driver.d.ts b/lib/appium-driver.d.ts index 35cf83a..3563261 100644 --- a/lib/appium-driver.d.ts +++ b/lib/appium-driver.d.ts @@ -10,21 +10,18 @@ import { Point } from "./point"; import { ImageHelper } from "./image-helper"; import { ImageOptions } from "./image-options"; import { LogType } from "./log-types"; +import { DeviceOrientation } from "./enums/device-orientation"; export declare class AppiumDriver { private _driver; private _wd; private _webio; private _driverConfig; private _args; - private static pngFileExt; - private static partialUrl; private _defaultWaitTime; private _elementHelper; private _imageHelper; private _isAlive; private _locators; - private _logPath; - private _storageByDeviceName; private _storageByPlatform; private constructor(); readonly imageHelper: ImageHelper; @@ -61,6 +58,7 @@ export declare class AppiumDriver { click(args: any): Promise; navBack(): Promise; static createAppiumDriver(args: INsCapabilities): Promise; + updateSettings(settings: any): Promise; /** * * @param xPath @@ -155,27 +153,35 @@ export declare class AppiumDriver { * @param xOffset * @param retryCount */ - scrollTo(direction: Direction, element: any, startPoint: Point, yOffset: number, xOffset?: number, retryCount?: number): Promise; + scrollTo(direction: Direction, element: any, startPoint: Point, offsetPoint: Point, retryCount?: number): Promise; /** - * Swipe from point with offset and inertia according to duatio + * Swipe from point with offset and inertia according to duration * @param y * @param x * @param yOffset * @param inertia * @param xOffset */ - swipe(y: number, x: number, yOffset: number, inertia?: number, xOffset?: number): Promise; + swipe(startPoint: { + x: number; + y: number; + }, endPoint: { + x: number; + y: number; + }, inertia?: number): Promise; /** * Click a point by providing coordinates * @param x * @param y */ clickPoint(xCoordinate: number, yCoordinate: number): Promise; + getOrientation(): Promise; + setOrientation(orientation: DeviceOrientation): Promise; source(): Promise; sessionId(): Promise; - compareElement(element: UIElement, imageName: string, tolerance?: number, timeOutSeconds?: number, toleranceType?: ImageOptions): Promise; - compareRectangle(rect: IRectangle, imageName: string, timeOutSeconds?: number, tolerance?: number, toleranceType?: ImageOptions): Promise; - compareScreen(imageName: string, timeOutSeconds?: number, tolerance?: number, toleranceType?: ImageOptions): Promise; + compareElement(element: UIElement, imageName?: string, tolerance?: number, timeOutSeconds?: number, toleranceType?: ImageOptions): Promise; + compareRectangle(rect: IRectangle, imageName?: string, timeOutSeconds?: number, tolerance?: number, toleranceType?: ImageOptions): Promise; + compareScreen(imageName?: string, timeOutSeconds?: number, tolerance?: number, toleranceType?: ImageOptions): Promise; /** * @param videoName * @param callback when to stop video recording. In order an element is found. Should return true to exit @@ -187,11 +193,10 @@ export declare class AppiumDriver { */ startRecordingVideo(videoName: any): any; stopRecordingVideo(): Promise; - private compare; - prepareImageToCompare(filePath: string, rect: IRectangle): Promise; takeScreenshot(fileName: string): Promise; + saveScreenshot(fileName: string): Promise; testReporterLog(log: any): any; - logScreenshot(fileName: string): Promise; + logScreenshot(fileName: string): Promise; getlog(logType: LogType): Promise; logPageSource(fileName: string): Promise; logDeviceLog(fileName: any, logType: LogType, filter?: string): Promise; @@ -202,21 +207,21 @@ export declare class AppiumDriver { logTestArtifacts(logName: string): Promise; /** * Send the currently active app to the background - * @param time in minutes + * @param time in seconds */ - backgroundApp(minutes: number): Promise; + backgroundApp(seconds: number): Promise; /** * Hides device keyboard */ hideDeviceKeyboard(): Promise; isKeyboardShown(): Promise; resetApp(): Promise; + restartApp(): Promise; init(): Promise; quit(): Promise; private static applyAdditionalSettings; private convertArrayToUIElements; private static configureLogging; - private getExpectedImagePath; /** * Wait specific amount of time before continue execution * @param milliseconds @@ -254,4 +259,14 @@ export declare class AppiumDriver { * @param imageThreshold The degree of match for current search, on the scale between 0 and 1. Default 0.4 */ findElementByImage(image: string, imageThreshold?: number): Promise; + /** + * Get screen actual view port + * Useful for image comparison + */ + getScreenActualViewPort(): IRectangle; + /** + * Get screen view port + * This is convenient to use for some gestures on the screen + */ + getScreenViewPort(): IRectangle; } diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index 1790c87..197114e 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -21,14 +21,10 @@ import { import { addExt, log, - getStorageByPlatform, - getStorageByDeviceName, resolvePath, - getReportPath, scroll, findFreePort, wait, - copy, getSessions, logError, prepareApp, @@ -45,24 +41,20 @@ import { IRectangle } from "./interfaces/rectangle"; import { Point } from "./point"; import { ImageHelper } from "./image-helper"; import { ImageOptions } from "./image-options" -import { unlinkSync, writeFileSync, existsSync } from "fs"; +import { writeFileSync, existsSync } from "fs"; import { DeviceManager } from "../lib/device-manager"; -import { extname, basename, join } from "path"; +import { extname, join } from "path"; import { LogType } from "./log-types"; import { screencapture } from "./helpers/screenshot-manager"; import { LogImageType } from "./enums/log-image-type"; +import { DeviceOrientation } from "./enums/device-orientation"; export class AppiumDriver { - private static pngFileExt = '.png'; - private static partialUrl = "/wd/hub/session/"; - private _defaultWaitTime: number = 5000; private _elementHelper: ElementHelper; private _imageHelper: ImageHelper; private _isAlive: boolean = false; private _locators: Locator; - private _logPath: string; - private _storageByDeviceName: string; private _storageByPlatform: string; private constructor(private _driver: any, private _wd, private _webio: any, private _driverConfig, private _args: INsCapabilities) { @@ -129,7 +121,7 @@ export class AppiumDriver { * Get the storage where test results from image comparison is logged It will be reports/app nam/device name */ get reportsPath() { - return this._logPath; + return this._args.reportsPath; } /** @@ -143,11 +135,11 @@ export class AppiumDriver { * Get the storage where images are captured. It will be resources/app nam/device name */ get storageByDeviceName() { - return this._storageByDeviceName; + return this._args.storageByDeviceName; } set storageByDeviceName(storageFullPath: string) { - this._storageByDeviceName = storageFullPath; + this._args.storageByDeviceName = storageFullPath; } get storage() { @@ -187,6 +179,17 @@ export class AppiumDriver { // } public static async createAppiumDriver(args: INsCapabilities) { + let appiumCapsFromConfig; + args.appiumCaps; + if (args.appiumCaps && args.appiumCaps.settings) { + appiumCapsFromConfig = {}; + Object.getOwnPropertyNames(args.appiumCaps).forEach(prop => { + appiumCapsFromConfig[prop] = args.appiumCaps[prop]; + }); + + delete args.appiumCaps.settings; + } + if (!args.isValidated) { await args.validateArgs(); } @@ -216,7 +219,7 @@ export class AppiumDriver { while (retries > 0 && !hasStarted) { try { let sessionInfo; - + let sessionInfoDetails; try { if (args.sessionId || args.attachToDebug) { const sessionInfos = JSON.parse(((await getSessions(args.port)) || "{}") + ''); @@ -225,13 +228,13 @@ export class AppiumDriver { if (!sessionInfo || !sessionInfo.id) { logError("No suitable session info found", sessionInfo); process.exit(1); + } else { + args.sessionId = sessionInfo.id; + await driver.attach(args.sessionId); + sessionInfoDetails = await driver.sessionCapabilities(); } - args.sessionId = sessionInfo.id; args.appiumCaps = sessionInfo.capabilities; - // remove app to prevent appium from installing app again - args.appiumCaps.app = ""; - if (sessionInfo.capabilities.automationName) { (args).setAutomationNameFromString(sessionInfo.capabilities.automationName); } @@ -243,13 +246,13 @@ export class AppiumDriver { } else { args.device = DeviceManager.getDefaultDevice(args); } + args.device = DeviceManager.applyAppiumSessionInfoDetails(args, sessionInfoDetails); } - - await driver.attach(args.sessionId); } else { sessionInfo = await driver.init(args.appiumCaps); + sessionInfoDetails = await driver.sessionCapabilities(); + args.device = DeviceManager.applyAppiumSessionInfoDetails(args, sessionInfoDetails); } - } catch (error) { args.verbose = true; if (!args.ignoreDeviceController && error && error.message && error.message.includes("Failure [INSTALL_FAILED_INSUFFICIENT_STORAGE]")) { @@ -257,12 +260,17 @@ export class AppiumDriver { await DeviceController.startDevice(args.device); } } - if (args.verbose) { - logInfo("Session info"); - console.info(sessionInfo); + logInfo("Session info: "); + console.info(sessionInfoDetails); + try { + logInfo("Appium settings: "); + console.log(await driver.settings()); + + } catch (error) { + logInfo("Current version of appium doesn't support appium settings!"); } - await DeviceManager.applyDeviceAdditionsSettings(driver, args, sessionInfo); + await DeviceManager.applyDeviceAdditionsSettings(driver, args, appiumCapsFromConfig); hasStarted = true; } catch (error) { @@ -276,27 +284,41 @@ export class AppiumDriver { args.appiumCaps["wdaLocalPort"] = freePort; } } + if (hasStarted) { console.log("Appium driver has started successfully!"); if (checkImageLogType(args.testReporter, LogImageType.screenshots)) { args.testReporterLog(`appium_driver_started`); - args.testReporterLog(screencapture(`${getReportPath(args)}/appium_driver_started.png`)); + args.testReporterLog(screencapture(`${args.reportsPath}/appium_driver_started.png`)); } } else { logError("Appium driver is NOT started!"); if (checkImageLogType(args.testReporter, LogImageType.screenshots)) { ensureReportsDirExists(args); args.testReporterLog(`appium_driver_boot_failure`); - args.testReporterLog(screencapture(`${getReportPath(args)}/appium_driver_boot_failure.png`)); + args.testReporterLog(screencapture(`${args.reportsPath}/appium_driver_boot_failure.png`)); } } retries--; } + try { + if (appiumCapsFromConfig && appiumCapsFromConfig.settings) { + appiumCapsFromConfig.settings = JSON.parse(appiumCapsFromConfig.settings); + } + } catch (error) { } + + if (appiumCapsFromConfig && appiumCapsFromConfig.settings) { + await driver.updateSettings(appiumCapsFromConfig.settings); + } return new AppiumDriver(driver, wd, webio, args.driverConfig, args); } + public async updateSettings(settings: any) { + this.driver.updateSettings(settings) + } + /** * * @param xPath @@ -458,7 +480,7 @@ export class AppiumDriver { * @param xOffset * @param retryCount */ - public async scrollTo(direction: Direction, element: any, startPoint: Point, yOffset: number, xOffset: number = 0, retryCount: number = 7) { + public async scrollTo(direction: Direction, element: any, startPoint: Point, offsetPoint: Point, retryCount: number = 7) { let el: UIElement = null; let isDisplayed: boolean = false; while ((el === null || !isDisplayed) && retryCount > 0) { @@ -466,7 +488,7 @@ export class AppiumDriver { el = await element(); isDisplayed = await el.isDisplayed(); if (!isDisplayed) { - await scroll(this._wd, this._driver, direction, this._webio.isIOS, startPoint.y, startPoint.x, yOffset, xOffset, this._args.verbose); + await scroll(this._wd, this._driver, direction, this._webio.isIOS, startPoint.y, startPoint.x, offsetPoint.x, offsetPoint.y, this._args.verbose); el = null; } } catch (error) { @@ -480,26 +502,26 @@ export class AppiumDriver { } /** - * Swipe from point with offset and inertia according to duatio + * Swipe from point with offset and inertia according to duration * @param y * @param x * @param yOffset * @param inertia * @param xOffset */ - public async swipe(y: number, x: number, yOffset: number, inertia: number = 250, xOffset: number = 0) { - let direction = 1; - if (this._webio.isIOS) { - direction = -1; - } - - const action = new this._wd.TouchAction(this._driver); - action - .press({ x: x, y: y }) + public async swipe(startPoint: { x: number, y: number }, endPoint: { x: number, y: number }, inertia?: number) { + if (!inertia) { + inertia = (Math.abs(startPoint.x - endPoint.x) > Math.abs(endPoint.y - startPoint.y) + ? Math.abs(startPoint.x - endPoint.x) : Math.abs(endPoint.y - startPoint.y)) + * 10; + } + new this._wd.TouchAction(this._driver) + .press({ x: startPoint.x, y: startPoint.y }) .wait(inertia) - .moveTo({ x: xOffset, y: direction * yOffset }) - .release(); - await action.perform(); + .moveTo({ x: endPoint.x, y: endPoint.y }) + .release() + .perform(); + await this._driver.sleep(150); } @@ -516,6 +538,24 @@ export class AppiumDriver { await this._driver.sleep(150); } + async getOrientation(): Promise { + return await this._driver.getOrientation(); + } + + public async setOrientation(orientation: DeviceOrientation) { + logInfo(`Set device orientation: ${orientation}`) + await this._driver.setOrientation(orientation); + + if (orientation === DeviceOrientation.LANDSCAPE) { + this.imageHelper.imageCropRect.x = this._imageHelper.options.cropRectangle.x; + this.imageHelper.imageCropRect.y = this._imageHelper.options.cropRectangle.y; + this.imageHelper.imageCropRect.width = this._imageHelper.options.cropRectangle.height; + this.imageHelper.imageCropRect.height = this._imageHelper.options.cropRectangle.width; + } else { + this.imageHelper.imageCropRect = undefined; + } + } + public async source() { return await this._webio.source(); } @@ -524,16 +564,35 @@ export class AppiumDriver { return await this.driver.getSessionId(); } - public async compareElement(element: UIElement, imageName: string, tolerance: number = 0.01, timeOutSeconds: number = 3, toleranceType?: ImageOptions) { + public async compareElement(element: UIElement, imageName?: string, tolerance: number = 0, timeOutSeconds: number = 3, toleranceType: ImageOptions = ImageOptions.percent) { return await this.compareRectangle(await element.getActualRectangle(), imageName, timeOutSeconds, tolerance, toleranceType); } - public async compareRectangle(rect: IRectangle, imageName: string, timeOutSeconds: number = 3, tolerance: number = 0.01, toleranceType?: ImageOptions) { - return await this.compare(imageName, timeOutSeconds, tolerance, rect, toleranceType); + public async compareRectangle(rect: IRectangle, imageName?: string, timeOutSeconds: number = 3, tolerance: number = 0, toleranceType: ImageOptions = ImageOptions.percent) { + imageName = imageName || this.imageHelper.testName; + const options = this.imageHelper.extendOptions({ + imageName: imageName, + timeOutSeconds: timeOutSeconds, + tolerance: tolerance, + cropRectangle: rect, + toleranceType: toleranceType, + keepOriginalImageName: true, + keepOriginalImageSize: false + }); + return await this.imageHelper.compare(options); } - public async compareScreen(imageName: string, timeOutSeconds: number = 3, tolerance: number = 0.01, toleranceType?: ImageOptions) { - return await this.compare(imageName, timeOutSeconds, tolerance, undefined, toleranceType); + public async compareScreen(imageName?: string, timeOutSeconds: number = 3, tolerance: number = 0, toleranceType: ImageOptions = ImageOptions.percent) { + imageName = imageName || this.imageHelper.testName; + const options = this.imageHelper.extendOptions({ + imageName: imageName, + timeOutSeconds: timeOutSeconds, + tolerance: tolerance, + toleranceType: toleranceType, + keepOriginalImageName: true + }); + + return await this.imageHelper.compare(options); } /** @@ -541,11 +600,7 @@ export class AppiumDriver { * @param callback when to stop video recording. In order an element is found. Should return true to exit */ public async recordVideo(videoName, callback: () => Promise): Promise { - if (!this._storageByDeviceName) { - this._storageByDeviceName = getStorageByDeviceName(this._args); - } - - return DeviceController.recordVideo((this._args.device), this._storageByDeviceName, videoName, callback); + return DeviceController.recordVideo((this._args.device), this._args.storageByDeviceName, videoName, callback); } private _recordVideoInfo; @@ -553,19 +608,15 @@ export class AppiumDriver { * @param videoName */ public startRecordingVideo(videoName) { - if (!this._logPath) { - this._logPath = getReportPath(this._args); - } - videoName = videoName.replace(/\s/gi, ""); console.log("DEVICE: ", this._args.device); - this._recordVideoInfo = DeviceController.startRecordingVideo(this._args.device, this._logPath, videoName); + this._recordVideoInfo = DeviceController.startRecordingVideo(this._args.device, this._args.reportsPath, videoName); this._recordVideoInfo['device'] = (this._args.device); return this._recordVideoInfo['pathToVideo']; } public stopRecordingVideo(): Promise { - this._recordVideoInfo['videoRecoringProcess'].kill("SIGINT"); + this._recordVideoInfo['videoRecordingProcess'].kill("SIGINT"); wait(this.isIOS ? 100 : 10000); if (this._args.device.type === DeviceType.EMULATOR || this._args.device.platform === Platform.ANDROID) { AndroidController.pullFile( @@ -578,97 +629,13 @@ export class AppiumDriver { return Promise.resolve(this._recordVideoInfo['pathToVideo']); } - private async compare(imageName: string, timeOutSeconds: number = 3, tolerance: number = 0.01, rect?: IRectangle, toleranceType?: ImageOptions) { - if (!this._logPath) { - this._logPath = getReportPath(this._args); - } - - imageName = addExt(imageName, AppiumDriver.pngFileExt); - - const pathExpectedImage = this.getExpectedImagePath(imageName); - - // First time capture - if (!existsSync(pathExpectedImage)) { - const pathActualImage = resolvePath(this._storageByDeviceName, this.imageHelper.options.preserveImageName ? imageName : imageName.replace(".", "_actual.")); - if (this.imageHelper.options.waitOnCreatingInitialSnapshot > 0) { - await this.wait(this.imageHelper.options.waitOnCreatingInitialSnapshot); - } - await this.takeScreenshot(pathActualImage); - - if (rect) { - await this._imageHelper.clipRectangleImage(rect, pathActualImage); - } - - const pathActualImageToReportsFolder = resolvePath(this._logPath, basename(pathActualImage)); - copy(pathActualImage, pathActualImageToReportsFolder, false); - - console.log("Remove the 'actual' suffix to continue using the image as expected one ", pathExpectedImage); - this._args.testReporterLog(basename(pathActualImage).replace(/\.\w{3,3}$/ig, "")); - this._args.testReporterLog(join(this._logPath, basename(pathActualImage))); - return false; - } - - // Compare - let pathActualImage = await this.takeScreenshot(resolvePath(this._logPath, imageName.replace(".", "_actual."))); - const pathDiffImage = pathActualImage.replace("actual", "diff"); - - await this.prepareImageToCompare(pathActualImage, rect); - let result = await this._imageHelper.compareImages(pathActualImage, pathExpectedImage, pathDiffImage, tolerance, toleranceType); - - // Iterate - if (!result) { - const eventStartTime = Date.now().valueOf(); - let counter = 1; - timeOutSeconds *= 1000; - while ((Date.now().valueOf() - eventStartTime) <= timeOutSeconds && !result) { - const pathActualImageConter = resolvePath(this._logPath, imageName.replace(".", "_actual_" + counter + ".")); - pathActualImage = await this.takeScreenshot(pathActualImageConter); - - await this.prepareImageToCompare(pathActualImage, rect); - result = await this._imageHelper.compareImages(pathActualImage, pathExpectedImage, pathDiffImage, tolerance, toleranceType); - if (checkImageLogType(this._args.testReporter, LogImageType.everyImage)) { - this._args.testReporterLog(`Actual image: ${basename(pathActualImage).replace(/\.\w{3,3}$/ig, "")}`); - this._args.testReporterLog(join(this._logPath, basename(pathActualImage))); - } - counter++; - } - - if (!checkImageLogType(this._args.testReporter, LogImageType.everyImage)) { - this._args.testReporterLog(`Actual image: ${basename(pathDiffImage).replace(/\.\w{3,3}$/ig, "")}`); - this._args.testReporterLog(join(this._logPath, basename(pathDiffImage))); - this._args.testReporterLog(`Actual image: ${basename(pathActualImage).replace(/\.\w{3,3}$/ig, "")}`); - this._args.testReporterLog(join(this._logPath, basename(pathActualImage))); - } - } else { - if (existsSync(pathDiffImage)) { - unlinkSync(pathDiffImage); - } - if (existsSync(pathActualImage)) { - unlinkSync(pathActualImage); - } - } - - this._imageHelper.imageCropRect = undefined; - return result; - } - - public async prepareImageToCompare(filePath: string, rect: IRectangle) { - if (rect) { - await this._imageHelper.clipRectangleImage(rect, filePath); - const rectToCrop = { x: 0, y: 0, width: undefined, height: undefined }; - this._imageHelper.imageCropRect = rectToCrop; - } else { - this._imageHelper.imageCropRect = ImageHelper.cropImageDefault(this._args); - } - } - public takeScreenshot(fileName: string) { - if (!fileName.endsWith(AppiumDriver.pngFileExt)) { - fileName = fileName.concat(AppiumDriver.pngFileExt); + if (!fileName.endsWith(ImageHelper.pngFileExt)) { + fileName = fileName.concat(ImageHelper.pngFileExt); } return new Promise((resolve, reject) => { - this._driver.takeScreenshot().then( + this._driver.takeScreenshot(fileName).then( function (image, err) { if (err) { console.error(err); @@ -681,6 +648,14 @@ export class AppiumDriver { }); } + public async saveScreenshot(fileName: string) { + if (!fileName.endsWith(ImageHelper.pngFileExt)) { + fileName = fileName.concat(ImageHelper.pngFileExt); + } + + return await this._driver.saveScreenshot(fileName); + } + public testReporterLog(log: any): any { if (this._args.testReporterLog) { return this._args.testReporterLog(log); @@ -689,22 +664,19 @@ export class AppiumDriver { } public async logScreenshot(fileName: string) { - if (!this._logPath) { - this._logPath = getReportPath(this._args); - } - if (!fileName.endsWith(AppiumDriver.pngFileExt)) { - fileName = fileName.concat(AppiumDriver.pngFileExt).replace(/\s+/ig, "_"); + if (!fileName.endsWith(ImageHelper.pngFileExt)) { + fileName = fileName.concat(ImageHelper.pngFileExt).replace(/\s+/ig, "_"); } if (Object.getOwnPropertyNames(this._args.testReporter).length > 0) { this.testReporterLog(fileName.replace(/\.\w+/ig, "")); - fileName = join(this._logPath, fileName); + fileName = join(this._args.reportsPath, fileName); fileName = this.testReporterLog(fileName); } - fileName = resolvePath(this._logPath, fileName) + fileName = resolvePath(this._args.reportsPath, fileName) - const imgPath = await this.takeScreenshot(fileName); + const imgPath = await this.saveScreenshot(fileName); return imgPath; } @@ -714,14 +686,11 @@ export class AppiumDriver { } public async logPageSource(fileName: string) { - if (!this._logPath) { - this._logPath = getReportPath(this._args); - } if (!fileName.endsWith(".xml")) { fileName = fileName.concat(".xml"); } - const path = resolvePath(this._logPath, fileName); + const path = resolvePath(this._args.reportsPath, fileName); const xml = await this.source(); writeFileSync(path, xml.value, 'utf8'); } @@ -735,9 +704,9 @@ export class AppiumDriver { } let deviceLog = "" logs.forEach(log => { - const curruntLog = `\n${JSON.stringify(log)}`; + const currentLog = `\n${JSON.stringify(log)}`; if (filter) { - if (curruntLog.includes(filter)) { + if (currentLog.includes(filter)) { deviceLog += `\n${JSON.stringify(log)}`; } } else { @@ -751,11 +720,7 @@ export class AppiumDriver { fileName = fileName.concat('_').concat(logType); fileName = fileName.concat(".log"); - if (!this._logPath) { - this._logPath = getReportPath(this._args); - } - - const path = resolvePath(this._logPath, fileName); + const path = resolvePath(this._args.reportsPath, fileName); writeFileSync(path, deviceLog, 'utf8'); } else { console.log(`Log type: ${logType} is empty!`); @@ -780,13 +745,13 @@ export class AppiumDriver { /** * Send the currently active app to the background - * @param time in minutes + * @param time in seconds */ - public async backgroundApp(minutes: number) { + public async backgroundApp(seconds: number) { logInfo("Sending the currently active app to the background ..."); this._args.testReporterLog("Sending the currently active app to the background ..."); - await this._driver.backgroundApp(minutes); + await this._driver.backgroundApp(seconds); } /** @@ -806,6 +771,22 @@ export class AppiumDriver { await this._driver.resetApp(); } + // restart app + public async restartApp() { + try { + await this._driver.closeApp(); + } catch (error) { + logError("Current appium version doesn't support closeApp()!"); + logError("Consider to use resetApp()! Reset app will reinstall the application"); + } + try { + await this._driver.launchApp(); + } catch (error) { + logError("Current appium version doesn't support launchApp()!"); + logError("Consider to use resetApp()! Reset app will reinstall the application"); + } + } + public async init() { await this._driver.init(this._args.appiumCaps); this._webio.requestHandler.sessionID = this._driver.sessionID; @@ -824,7 +805,7 @@ export class AppiumDriver { console.log("Driver is dead!"); if (checkImageLogType(this._args.testReporter, LogImageType.screenshots)) { this._args.testReporterLog(`appium_driver_quit`); - this._args.testReporterLog(screencapture(`${getReportPath(this._args)}/appium_driver_quit.png`)); + this._args.testReporterLog(screencapture(`${this._args.reportsPath}/appium_driver_quit.png`)); } } else { //await this._webio.detach(); @@ -833,7 +814,7 @@ export class AppiumDriver { if (this._args.verbose) { if (checkImageLogType(this._args.testReporter, LogImageType.screenshots)) { this._args.testReporterLog(`appium_driver_quit_failure`); - this._args.testReporterLog(screencapture(`${getReportPath(this._args)}/appium_driver_quit_failure.png`)); + this._args.testReporterLog(screencapture(`${this._args.reportsPath}/appium_driver_quit_failure.png`)); } console.dir(error); } @@ -898,27 +879,6 @@ export class AppiumDriver { }); }; - private getExpectedImagePath(imageName: string) { - if (!this._storageByDeviceName) { - this._storageByDeviceName = getStorageByDeviceName(this._args); - } - - let pathExpectedImage = resolvePath(this._storageByDeviceName, imageName); - - if (!existsSync(pathExpectedImage)) { - if (!this._storageByPlatform) { - this._storageByPlatform = getStorageByPlatform(this._args); - } - pathExpectedImage = resolvePath(this._storageByPlatform, imageName); - } - - if (!existsSync(pathExpectedImage)) { - pathExpectedImage = resolvePath(this._storageByDeviceName, imageName); - } - - return pathExpectedImage; - } - /** * Wait specific amount of time before continue execution * @param milliseconds @@ -997,8 +957,8 @@ export class AppiumDriver { */ public async findElementByImage(image: string, imageThreshold = 0.4) { await this._driver.updateSettings({ imageMatchThreshold: imageThreshold }); - const imageName = addExt(image, AppiumDriver.pngFileExt); - const pathExpectedImage = this.getExpectedImagePath(imageName); + const imageName = addExt(image, ImageHelper.pngFileExt); + const pathExpectedImage = this._imageHelper.getExpectedImagePathByDevice(imageName); if (!existsSync(pathExpectedImage)) { throw new Error("The provided image does not exist!!!"); @@ -1016,4 +976,28 @@ export class AppiumDriver { return new UIElement(searchResult, this._driver, this._wd, this._webio, this._args, "elementByImage", imageAsBase64); } + + /** + * Get screen actual view port + * Useful for image comparison + */ + public getScreenActualViewPort(): IRectangle { + return (this._args.appiumCaps && this._args.appiumCaps.viewportRect) || this._args.device.viewportRect; + } + + /** + * Get screen view port + * This is convenient to use for some gestures on the screen + */ + public getScreenViewPort(): IRectangle { + const rect = (this._args.appiumCaps && this._args.appiumCaps.viewportRect) || this._args.device.viewportRect; + if (rect && Object.getOwnPropertyNames(rect).length > 0) { + return { + x: rect.x / this._args.appiumCaps.device.deviceScreenDensity, + y: rect.y / this._args.appiumCaps.device.deviceScreenDensity, + width: rect.x / this._args.appiumCaps.device.deviceScreenDensity, + height: rect.x / this._args.appiumCaps.device.deviceScreenDensity, + } + } + } } \ No newline at end of file diff --git a/lib/appium-server.ts b/lib/appium-server.ts index f766689..e22ef5e 100644 --- a/lib/appium-server.ts +++ b/lib/appium-server.ts @@ -225,7 +225,7 @@ export class AppiumServer { logWarn("Using global Appium binary."); // console.log('Please, make sure it is installed globally.'); //} else if (result.includes("appium")) { - // const msg = "Appium not found. Please install appium before runnig tests!"; + // const msg = "Appium not found. Please install appium before running tests!"; // log(msg, this._args.verbose); // new Error(msg); // } diff --git a/lib/device-manager.d.ts b/lib/device-manager.d.ts index 5731870..f0bdbe6 100644 --- a/lib/device-manager.d.ts +++ b/lib/device-manager.d.ts @@ -12,6 +12,8 @@ export declare class DeviceManager implements IDeviceManager { static kill(device: IDevice): Promise; static getInstalledApps(device: IDevice): Promise; static getDefaultDevice(args: INsCapabilities, deviceName?: string, token?: string, type?: DeviceType, platformVersion?: number): IDevice; + private static convertViewportRectToIRectangle; + static applyAppiumSessionInfoDetails(args: INsCapabilities, sessionInfoDetails: any): any; static setDontKeepActivities(args: INsCapabilities, driver: any, value: any): Promise; static executeShellCommand(driver: any, commandArgs: { command: string; diff --git a/lib/device-manager.ts b/lib/device-manager.ts index 55fc052..148f7be 100644 --- a/lib/device-manager.ts +++ b/lib/device-manager.ts @@ -12,6 +12,7 @@ import { sortDescByApiLevelPredicate } from "mobile-devices-controller"; import { isRegExp } from "util"; +import { NsCapabilities } from "./ns-capabilities"; export class DeviceManager implements IDeviceManager { private static _emulators: Map = new Map(); @@ -98,7 +99,7 @@ export class DeviceManager implements IDeviceManager { logInfo("Device is connected:", device) } if (device.status === Status.SHUTDOWN) { - await DeviceController.startDevice(device, startDeviceOptions, shouldFullyResetDevice); + device = await DeviceController.startDevice(device, startDeviceOptions, shouldFullyResetDevice); try { delete device.process; } catch (error) { } @@ -181,6 +182,38 @@ export class DeviceManager implements IDeviceManager { return device; } + private static convertViewportRectToIRectangle(viewportRect) { + if (!viewportRect) { + return viewportRect; + } + return { + x: viewportRect.left, + y: viewportRect.top, + width: viewportRect.width, + height: viewportRect.height, + }; + } + + public static applyAppiumSessionInfoDetails(args: INsCapabilities, sessionInfoDetails) { + if (args.isAndroid) { + const sizeArr = sessionInfoDetails.deviceScreenSize.split("x"); + args.device.deviceScreenSize = { width: sizeArr[0], height: sizeArr[1] }; + + args.device.apiLevel = sessionInfoDetails.deviceApiLevel; + args.device.deviceScreenDensity = sessionInfoDetails.deviceScreenDensity / 100; + args.device.config = { "density": args.device.deviceScreenDensity, "offsetPixels": +sessionInfoDetails.statBarHeight }; + } else { + args.device.apiLevel = sessionInfoDetails.platformVersion; + args.device.deviceScreenDensity = sessionInfoDetails.pixelRatio; + args.device.config = { "density": sessionInfoDetails.pixelRatio, "offsetPixels": +sessionInfoDetails.viewportRect.top - +sessionInfoDetails.statBarHeight }; + } + + args.device.statBarHeight = sessionInfoDetails.statBarHeight; + args.device.viewportRect = DeviceManager.convertViewportRectToIRectangle(sessionInfoDetails.viewportRect); + + return args.device; + } + public static async setDontKeepActivities(args: INsCapabilities, driver, value) { const status = value ? 1 : 0; try { @@ -218,7 +251,7 @@ export class DeviceManager implements IDeviceManager { if (args.relaxedSecurity && !args.device.config.density) { const d = await DeviceManager.executeShellCommand(driver, { command: "wm", args: ["density"] }); args.device.config.density = /\d+/ig.test(d) ? parseInt(/\d+/ig.exec(d)[0]) / 100 : NaN; - console.log(`Device density recieved from adb shell command ${args.device.config.density}`); + console.log(`Device density received from adb shell command ${args.device.config.density}`); } if (args.device.config.density) { @@ -236,8 +269,15 @@ export class DeviceManager implements IDeviceManager { } } + // public static async applyDeviceAdditionsSettings(args: INsCapabilities, appiumCaps: any) { + // if (appiumCaps) { + // args.device.config.offsetPixels = appiumCaps.offsetPixels || args.device.config.offsetPixels; + // args.device.config.density = appiumCaps.density || args.device.config.density; + // } + // } + public static async applyDeviceAdditionsSettings(driver, args: INsCapabilities, sessionInfo: any) { - if (!args.device.config || !args.device.config.offsetPixels) { + if ((!args.device.viewportRect || !args.device.viewportRect.x) && (!args.device.config || !args.device.config.offsetPixels)) { args.device.config = {}; let density: number; if (sessionInfo && sessionInfo.length >= 1) { diff --git a/lib/element-helper.d.ts b/lib/element-helper.d.ts index a056fb2..4dc93ed 100644 --- a/lib/element-helper.d.ts +++ b/lib/element-helper.d.ts @@ -8,5 +8,5 @@ export declare class ElementHelper { getXPathWithExactText(text: any): string; getXPathContainingText(text: any): string; findByTextLocator(controlType: any, value: any, exactMatch: any): string; - getXPathByTextAtributes(controlType: any, textValue: any, exactMatch: any): string; + getXPathByTextAttributes(controlType: any, textValue: any, exactMatch: any): string; } diff --git a/lib/element-helper.ts b/lib/element-helper.ts index 5bbc851..29bcae5 100644 --- a/lib/element-helper.ts +++ b/lib/element-helper.ts @@ -28,28 +28,28 @@ export class ElementHelper { } public findByTextLocator(controlType, value, exactMatch) { - const result = this.getXPathByTextAtributes(`//${controlType}`, value, exactMatch); + const result = this.getXPathByTextAttributes(`//${controlType}`, value, exactMatch); return result; } - public getXPathByTextAtributes(controlType, textValue, exactMatch) { - let artbutes = ["label", "value", "hint"]; + public getXPathByTextAttributes(controlType, textValue, exactMatch) { + let attributes = ["label", "value", "hint"]; if (this._args.isAndroid) { - artbutes = ["content-desc", "resource-id", "text"]; + attributes = ["content-desc", "resource-id", "text"]; } let searchedString = ""; if (exactMatch) { if (this._args.isAndroid) { - artbutes.forEach((atr) => { searchedString += "translate(@" + atr + ",'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='" + textValue.toLowerCase() + "'" + " or " }); + attributes.forEach((atr) => { searchedString += "translate(@" + atr + ",'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='" + textValue.toLowerCase() + "'" + " or " }); } else { - artbutes.forEach((atr) => { searchedString += "@" + atr + "='" + textValue + "'" + " or " }); + attributes.forEach((atr) => { searchedString += "@" + atr + "='" + textValue + "'" + " or " }); } } else { if (this._args.isAndroid) { - artbutes.forEach((atr) => { searchedString += "contains(translate(@" + atr + ",'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'" + textValue.toLowerCase() + "')" + " or " }); + attributes.forEach((atr) => { searchedString += "contains(translate(@" + atr + ",'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'" + textValue.toLowerCase() + "')" + " or " }); } else { - artbutes.forEach((atr) => { searchedString += "contains(@" + atr + ",'" + textValue + "')" + " or " }); + attributes.forEach((atr) => { searchedString += "contains(@" + atr + ",'" + textValue + "')" + " or " }); } } diff --git a/lib/enums/device-orientation.d.ts b/lib/enums/device-orientation.d.ts new file mode 100644 index 0000000..94df5de --- /dev/null +++ b/lib/enums/device-orientation.d.ts @@ -0,0 +1,4 @@ +export declare enum DeviceOrientation { + LANDSCAPE = "LANDSCAPE", + PORTRAIT = "PORTRAIT" +} diff --git a/lib/enums/device-orientation.ts b/lib/enums/device-orientation.ts new file mode 100644 index 0000000..7307059 --- /dev/null +++ b/lib/enums/device-orientation.ts @@ -0,0 +1,4 @@ +export enum DeviceOrientation { + LANDSCAPE = "LANDSCAPE", + PORTRAIT = "PORTRAIT", +} \ No newline at end of file diff --git a/lib/frame-comparer.ts b/lib/frame-comparer.ts index 19358aa..ef2f649 100644 --- a/lib/frame-comparer.ts +++ b/lib/frame-comparer.ts @@ -2,7 +2,6 @@ import * as frComparer from "frame-comparer"; import { resolvePath, getStorageByDeviceName, getReportPath } from "./utils"; import { INsCapabilities } from "./interfaces/ns-capabilities"; import { IRectangle } from ".."; -import { ImageHelper } from "./image-helper"; export function loadFrameComparer(nsCapabilities: INsCapabilities) { try { @@ -19,7 +18,7 @@ export class FrameComparer { private _cropImageRect: IRectangle; constructor(private _nsCapabilities: INsCapabilities, private _storage: string, private _logPath: string) { - this._cropImageRect = ImageHelper.cropImageDefault(this._nsCapabilities); + this._cropImageRect = this._nsCapabilities.appiumCaps.viewportRect || this._nsCapabilities.device.viewportRect; } async processVideo(videoFullName, framesGeneralName?: string, videoTempStorage = "tempFramesFolder") { diff --git a/lib/image-helper.d.ts b/lib/image-helper.d.ts index bcbe9d6..e29a86f 100644 --- a/lib/image-helper.d.ts +++ b/lib/image-helper.d.ts @@ -6,51 +6,102 @@ import { AppiumDriver } from "./appium-driver"; export interface IImageCompareOptions { imageName?: string; timeOutSeconds?: number; + /** + * pixel + * percentage thresholds: 1 = 100%, 0.2 = 20%" + */ tolerance?: number; + /** + * pixel + * percentage thresholds: 1 = 100%, 0.2 = 20%" + */ toleranceType?: ImageOptions; /** - * wait miliseconds before capture creating image + * Wait miliseconds before capture creating image + * Default value is 2000 + */ + waitBeforeCreatingInitialImageCapture?: number; + /** + * This property will preserve not to add be added _actual postfix on initial image capture + */ + donNotAppendActualSuffixOnIntialImageCapture?: boolean; + /** + * This property will ensure that the image name will not be manipulated with count postfix. + * This is very convenient in order to reuses image. + * Default value is false. + */ + keepOriginalImageName?: boolean; + /** + * Clip image before compare. Default value excludes status bar(both android and ios) and software buttons(android). */ - waitOnCreatingInitialSnapshot?: number; + cropRectangle?: IRectangle; /** - * This property will keep image name as it is and will not add _actual postfix on initial capture + * Default value is set to true which means that nativescript-dev-appium will save the image + * in original size and compare only the part which cropRectangle specifies. + * If false, the image size will be reduced and saved by the dimensions of cropRectangle. */ - preserveImageName?: boolean; + keepOriginalImageSize?: boolean; + /** + * Default value is set to false. nativescript-dev-appium will recalculate view port for iOS + * so that the top/y will start from the end of status bar + * So far appium calculates it even more and some part of safe areas are missed + */ + keepAppiumViewportRect?: boolean; + /** + * Defines if an image is device specific or only by platform. + * Default value is true and the image will be saved in device specific directory. + * If value is set to false, image will be saved under ios or android folder. + */ + isDeviceSpecific?: boolean; + /** + * Overwrite actual image if doesn't match. Default value is false. + */ + overwriteActualImage?: boolean; } export declare class ImageHelper { private _args; private _driver; - private _imageCropRect; private _blockOutAreas; private _imagesResults; - private _testName; + private _imageCropRect; private _options; + private _defaultOptions; constructor(_args: INsCapabilities, _driver: AppiumDriver); - options: IImageCompareOptions; + static readonly pngFileExt = ".png"; testName: string; - imageComppareOptions: IImageCompareOptions; + /** + * Defines when an image output should be created. + * This can be for different images, similar or different images, or all comparisons. + * (default: BlinkDiff.OUTPUT_ALL) + */ + imageOutputLimit: ImageOptions; + /** + * Max. distance colors in the 4 dimensional color-space without triggering a difference. (default: 20) + */ + delta: number; + options: IImageCompareOptions; + imageCropRect: IRectangle; + blockOutAreas: IRectangle[]; compareScreen(options?: IImageCompareOptions): Promise; compareElement(element: UIElement, options?: IImageCompareOptions): Promise; - compareRectangle(element: IRectangle, options?: IImageCompareOptions): Promise; + compareRectangle(cropRectangle: IRectangle, options?: IImageCompareOptions): Promise; hasImageComparisonPassed(): boolean; + /** + * Reset image comparison results + */ reset(): void; - private increaseImageName; - private extendOptions; - imageCropRect: IRectangle; - blockOutAreas: IRectangle[]; - imageOutputLimit(): ImageOptions; - thresholdType(): ImageOptions; - threshold(thresholdType: any): 0.01 | 10; - delta(): number; - static cropImageDefault(_args: INsCapabilities): { - x: number; - y: any; - width: any; - height: any; - }; - private static getOffsetPixels; - private runDiff; - compareImages(actual: string, expected: string, output: string, valueThreshold?: number, typeThreshold?: any): Promise; + /** + * Set comparison option to default + */ + resetDefaultOptions(): void; + getExpectedImagePathByDevice(imageName: string): string; + getExpectedImagePathByPlatform(imageName: string): string; + compare(options: IImageCompareOptions): Promise; + compareImages(actual: string, expected: string, output: string, tolerance: number, toleranceType: ImageOptions): Promise; clipRectangleImage(rect: IRectangle, path: string): Promise<{}>; readImage(path: string): Promise; + private runDiff; + private increaseImageName; + extendOptions(options: IImageCompareOptions): IImageCompareOptions; + private static fullClone; } diff --git a/lib/image-helper.ts b/lib/image-helper.ts index 37f40f2..9e5b968 100644 --- a/lib/image-helper.ts +++ b/lib/image-helper.ts @@ -1,4 +1,3 @@ -import { basename } from "path"; import * as BlinkDiff from "blink-diff"; import * as PngJsImage from "pngjs-image"; import { ImageOptions } from "./image-options"; @@ -7,40 +6,129 @@ import { IRectangle } from "./interfaces/rectangle"; import { LogImageType } from "./enums/log-image-type"; import { UIElement } from "./ui-element"; import { AppiumDriver } from "./appium-driver"; -import { logError } from "./utils"; +import { logError, checkImageLogType, resolvePath, copy, addExt, logWarn } from "./utils"; +import { unlinkSync, existsSync, mkdirSync } from "fs"; +import { basename, join } from "path"; +import { isObject } from "util"; +import { logInfo } from "mobile-devices-controller/lib/utils"; export interface IImageCompareOptions { imageName?: string; timeOutSeconds?: number; + + /** + * pixel + * percentage thresholds: 1 = 100%, 0.2 = 20%" + */ tolerance?: number; + + /** + * pixel + * percentage thresholds: 1 = 100%, 0.2 = 20%" + */ toleranceType?: ImageOptions; + + /** + * Wait miliseconds before capture creating image + * Default value is 2000 + */ + waitBeforeCreatingInitialImageCapture?: number; + + /** + * This property will preserve not to add be added _actual postfix on initial image capture + */ + donNotAppendActualSuffixOnIntialImageCapture?: boolean; + + /** + * This property will ensure that the image name will not be manipulated with count postfix. + * This is very convenient in order to reuses image. + * Default value is false. + */ + keepOriginalImageName?: boolean; + /** - * wait miliseconds before capture creating image + * Clip image before compare. Default value excludes status bar(both android and ios) and software buttons(android). */ - waitOnCreatingInitialSnapshot?: number; + cropRectangle?: IRectangle; + + /** + * Default value is set to true which means that nativescript-dev-appium will save the image + * in original size and compare only the part which cropRectangle specifies. + * If false, the image size will be reduced and saved by the dimensions of cropRectangle. + */ + keepOriginalImageSize?: boolean; + + + /** + * Default value is set to false. nativescript-dev-appium will recalculate view port for iOS + * so that the top/y will start from the end of status bar + * So far appium calculates it even more and some part of safe areas are missed + */ + keepAppiumViewportRect?: boolean; + + /** + * Defines if an image is device specific or only by platform. + * Default value is true and the image will be saved in device specific directory. + * If value is set to false, image will be saved under ios or android folder. + */ + isDeviceSpecific?: boolean; + /** - * This property will keep image name as it is and will not add _actual postfix on initial capture + * Overwrite actual image if doesn't match. Default value is false. */ - preserveImageName?: boolean; + overwriteActualImage?: boolean; } export class ImageHelper { - - private _imageCropRect: IRectangle; private _blockOutAreas: IRectangle[]; private _imagesResults = new Map(); - private _testName: string; - private _options: IImageCompareOptions = { + private _imageCropRect: IRectangle; + private _options: IImageCompareOptions = {}; + private _defaultOptions: IImageCompareOptions = { timeOutSeconds: 2, tolerance: 0, toleranceType: ImageOptions.pixel, - waitOnCreatingInitialSnapshot: 2000, - preserveImageName: false, + waitBeforeCreatingInitialImageCapture: 5000, + donNotAppendActualSuffixOnIntialImageCapture: false, + keepOriginalImageSize: true, + keepOriginalImageName: false, + isDeviceSpecific: true, + cropRectangle: {}, + imageName: undefined, + overwriteActualImage: false, }; constructor(private _args: INsCapabilities, private _driver: AppiumDriver) { + this._defaultOptions.cropRectangle = (this._args.appiumCaps && this._args.appiumCaps.viewportRect) || this._args.device.viewportRect; + if (!this._defaultOptions.cropRectangle + || this._defaultOptions.cropRectangle.y === undefined + || this._defaultOptions.cropRectangle.y === null + || this._defaultOptions.cropRectangle.y === NaN) { + this._defaultOptions.cropRectangle = this._defaultOptions.cropRectangle || {}; + this._defaultOptions.cropRectangle.y = this._args.device.config.offsetPixels || 0; + this._defaultOptions.cropRectangle.x = 0; + } + ImageHelper.fullClone(this._defaultOptions, this._options); + + logInfo(`Actual view port:`, this._options); } + public static readonly pngFileExt = '.png'; + + public testName: string; + + /** + * Defines when an image output should be created. + * This can be for different images, similar or different images, or all comparisons. + * (default: BlinkDiff.OUTPUT_ALL) + */ + public imageOutputLimit = ImageOptions.outputAll; + + /** + * Max. distance colors in the 4 dimensional color-space without triggering a difference. (default: 20) + */ + public delta: number = 20; + get options() { return this._options; } @@ -49,47 +137,46 @@ export class ImageHelper { this._options = this.extendOptions(options); } - set testName(testName: string) { - this._testName = testName; + get imageCropRect(): IRectangle { + return this._imageCropRect || this.options.cropRectangle; } - get testName() { - return this._testName; + set imageCropRect(clipRectangle: IRectangle) { + this._imageCropRect = clipRectangle; } - get imageComppareOptions() { - this.extendOptions(this._options); - - return this._options; + get blockOutAreas() { + return this._blockOutAreas; } - set imageComppareOptions(imageComppareOptions: IImageCompareOptions) { - this._options = this.extendOptions(imageComppareOptions); + set blockOutAreas(rectangles: IRectangle[]) { + this._blockOutAreas = rectangles; } public async compareScreen(options?: IImageCompareOptions) { options = this.extendOptions(options); - const imageName = this.increaseImageName(options.imageName || this._testName); - const result = await this._driver.compareScreen(imageName, options.timeOutSeconds, options.tolerance, options.toleranceType); - this._imagesResults.set(imageName, result); + options.imageName = this.increaseImageName(options.imageName || this.testName, options); + const result = await this.compare(options); + this._imagesResults.set(options.imageName, result); return result; } public async compareElement(element: UIElement, options?: IImageCompareOptions) { options = this.extendOptions(options); - const imageName = this.increaseImageName(options.imageName || this._testName); - const result = await this._driver.compareElement(element, imageName, options.tolerance, options.timeOutSeconds, options.toleranceType); - this._imagesResults.set(imageName, result); + options.imageName = this.increaseImageName(options.imageName || this.testName, options); + const cropRectangle = await element.getActualRectangle(); + const result = await this.compareRectangle(cropRectangle, options); return result; } - public async compareRectangle(element: IRectangle, options?: IImageCompareOptions) { + public async compareRectangle(cropRectangle: IRectangle, options?: IImageCompareOptions) { options = this.extendOptions(options); - const imageName = this.increaseImageName(options.imageName || this._testName); - const result = await this._driver.compareRectangle(element, imageName, options.timeOutSeconds, options.tolerance, options.toleranceType); - this._imagesResults.set(imageName, result); + options.imageName = this.increaseImageName(options.imageName || this.testName, options); + options.cropRectangle = cropRectangle; + const result = await this.compare(options); + this._imagesResults.set(options.imageName, result); return result; } @@ -109,147 +196,160 @@ export class ImageHelper { return shouldFailTest; } + /** + * Reset image comparison results + */ public reset() { this._imagesResults.clear(); this.testName = undefined; } - private increaseImageName(imageName: string) { - if (!imageName) { - logError(`Missing image name!`); - logError(`Consider to set - drive.imageHelper.testName - dirver.imageHelper.options.imageName - `); - throw new Error(`Missing image name!`) - } - if (this._imagesResults.size > 0) { - const images = new Array(); - this._imagesResults.forEach((v, k, map) => { - if (k.includes(imageName)) { - images.push(k); - } - }); + /** + * Set comparison option to default + */ + public resetDefaultOptions() { + ImageHelper.fullClone(this._defaultOptions, this._options); + } - images.sort((a, b) => { - const aNumber = +/\d+$/.exec(a); - const bNumber = +/\d+$/.exec(b); + public getExpectedImagePathByDevice(imageName: string) { + let pathExpectedImage = resolvePath(this._args.storageByDeviceName, imageName); - return bNumber - aNumber; - }); - if (images.length > 0) { - const lastImage = images[0]; - const number = /\d+$/.test(lastImage) ? +`${/\d+$/.exec(lastImage)}` + 1 : `2`; - imageName = `${imageName}_${number}`; - } - } + return pathExpectedImage; + } - return imageName; + public getExpectedImagePathByPlatform(imageName: string) { + let pathExpectedImage = resolvePath(this._args.storageByPlatform, imageName); + return pathExpectedImage; } - private extendOptions(options: IImageCompareOptions) { - options = options || {}; - Object.getOwnPropertyNames(this.options).forEach(prop => { - if (!options[prop]) { - options[prop] = this.options[prop]; - } - }); + public async compare(options: IImageCompareOptions) { + let imageName = addExt(options.imageName, ImageHelper.pngFileExt); + const storageLocal = options.isDeviceSpecific ? this._args.storageByDeviceName : this._args.storageByPlatform; + const pathExpectedImage = options.isDeviceSpecific ? this.getExpectedImagePathByDevice(imageName) : this.getExpectedImagePathByPlatform(imageName); - return options; - } + if (!existsSync(this._args.reportsPath)) { + mkdirSync(this._args.reportsPath); + } - get imageCropRect(): IRectangle { - return this._imageCropRect; - } + const captureFirstImage = async () => { + const pathActualImage = resolvePath(storageLocal, (this.options.donNotAppendActualSuffixOnIntialImageCapture || this.options.overwriteActualImage) ? imageName : imageName.replace(".", "_actual.")); + if (this.options.waitBeforeCreatingInitialImageCapture > 0) { + await this._driver.wait(this.options.waitBeforeCreatingInitialImageCapture); + } + await this._driver.saveScreenshot(pathActualImage); - set imageCropRect(rect: IRectangle) { - this._imageCropRect = rect; - } + if (!options.keepOriginalImageSize) { + await this.clipRectangleImage(options.cropRectangle, pathActualImage); + } - get blockOutAreas() { - return this._blockOutAreas; - } + const pathActualImageToReportsFolder = resolvePath(this._args.reportsPath, basename(pathActualImage)); + copy(pathActualImage, pathActualImageToReportsFolder, false); - set blockOutAreas(rectangles: IRectangle[]) { - this._blockOutAreas = rectangles; - } + if (this.options.donNotAppendActualSuffixOnIntialImageCapture || this.options.overwriteActualImage) { + logWarn(`New image ${basename(pathActualImage)} is saved to storage ${storageLocal}.`, pathExpectedImage); + } else if (this.options.donNotAppendActualSuffixOnIntialImageCapture === false && this.options.overwriteActualImage === false) { + logWarn("Remove the 'actual' suffix to continue using the image as expected one ", pathExpectedImage); + } + this._args.testReporterLog(basename(pathActualImage).replace(/\.\w{3,3}$/ig, "")); + this._args.testReporterLog(join(this._args.reportsPath, basename(pathActualImage))); + return false; + } + // First time capture + if (!existsSync(pathExpectedImage)) { + await captureFirstImage(); - public imageOutputLimit() { - return ImageOptions.outputAll; - } + return false; + } - public thresholdType() { - return ImageOptions.percent; - } + // Compare + let pathActualImage = await this._driver.saveScreenshot(resolvePath(this._args.reportsPath, imageName.replace(".", "_actual."))); + if (!options.keepOriginalImageSize) { + await this.clipRectangleImage(options.cropRectangle, pathActualImage); + } + const pathDiffImage = pathActualImage.replace("actual", "diff"); + + // await this.prepareImageToCompare(pathActualImage, options.cropRectangle); + let result = await this.compareImages(pathActualImage, pathExpectedImage, pathDiffImage, options.tolerance, options.toleranceType); + + // Iterate + if (!result) { + const eventStartTime = Date.now().valueOf(); + let counter = 1; + options.timeOutSeconds *= 1000; + while ((Date.now().valueOf() - eventStartTime) <= options.timeOutSeconds && !result) { + const pathActualImageCounter = resolvePath(this._args.reportsPath, imageName.replace(".", "_actual_" + counter + ".")); + pathActualImage = await this._driver.saveScreenshot(pathActualImageCounter); + if (!options.keepOriginalImageSize) { + await this.clipRectangleImage(options.cropRectangle, pathActualImage); + } + // await this.prepareImageToCompare(pathActualImage, this.imageCropRect); + result = await this.compareImages(pathActualImage, pathExpectedImage, pathDiffImage, options.tolerance, options.toleranceType); + if (!result && checkImageLogType(this._args.testReporter, LogImageType.everyImage)) { + this._args.testReporterLog(`Actual image: ${basename(pathActualImage).replace(/\.\w{3,3}$/ig, "")}`); + this._args.testReporterLog(join(this._args.reportsPath, basename(pathActualImage))); + } + counter++; + } - public threshold(thresholdType) { - if (thresholdType == ImageOptions.percent) { - return 0.01; // 0.01 = 1 percent; 500 percent + if (options.overwriteActualImage === true && !result) { + logError(`Overwrite image ${pathExpectedImage}, since overwriteActualImage option is set to true!`); + await captureFirstImage(); + } + + if (!result && !checkImageLogType(this._args.testReporter, LogImageType.everyImage)) { + this._args.testReporterLog(`${basename(pathDiffImage).replace(/\.\w{3,3}$/ig, "")}`); + this._args.testReporterLog(join(this._args.reportsPath, basename(pathDiffImage))); + this._args.testReporterLog(`Actual image: ${basename(pathActualImage).replace(/\.\w{3,3}$/ig, "")}`); + this._args.testReporterLog(join(this._args.reportsPath, basename(pathActualImage))); + } } else { - return 10; // 10 pixels + if (existsSync(pathDiffImage)) { + unlinkSync(pathDiffImage); + } + if (existsSync(pathActualImage)) { + unlinkSync(pathActualImage); + } } - } - public delta() { - return 20; - } - - public static cropImageDefault(_args: INsCapabilities) { - return { x: 0, y: ImageHelper.getOffsetPixels(_args), width: undefined, height: undefined }; + return result; } - private static getOffsetPixels(args: INsCapabilities) { - return args.device.config ? args.device.config.offsetPixels : 0 - } + public compareImages(actual: string, expected: string, output: string, tolerance: number, toleranceType: ImageOptions) { + const clipRect = { + x: this.imageCropRect.x, + y: this.imageCropRect.y, + width: this.imageCropRect.width, + height: this.imageCropRect.height + } - private runDiff(diffOptions: BlinkDiff, diffImage: string) { - var that = this; - return new Promise((resolve, reject) => { - diffOptions.run(function (error, result) { - if (error) { - throw error; - } else { - let message: string; - let resultCode = diffOptions.hasPassed(result.code); - if (resultCode) { - message = "Screen compare passed!"; - console.log(message); - console.log('Found ' + result.differences + ' differences.'); - return resolve(true); - } else { - message = `Screen compare failed! Found ${result.differences} differences.\n`; - console.log(message); - if (Object.getOwnPropertyNames(that._args.testReporter).length > 0 && that._args.testReporter.logImageTypes && that._args.testReporter.logImageTypes.indexOf(LogImageType.everyImage) > -1) { - that._args.testReporterLog(`${basename(diffImage)}\n\r${message}`); - that._args.testReporterLog(diffImage); - } - return resolve(false); - } - } - }); - }); - } + if (!this.options.keepOriginalImageSize) { + clipRect.x = 0; + clipRect.y = 0; + clipRect.width = undefined; + clipRect.height = undefined; + } - public compareImages(actual: string, expected: string, output: string, valueThreshold: number = this.threshold(this.thresholdType()), typeThreshold: any = this.thresholdType()) { const diff = new BlinkDiff({ imageAPath: actual, imageBPath: expected, imageOutputPath: output, - imageOutputLimit: this.imageOutputLimit(), - thresholdType: typeThreshold, - threshold: valueThreshold, - delta: this.delta(), - cropImageA: this._imageCropRect, - cropImageB: this._imageCropRect, + imageOutputLimit: this.imageOutputLimit, + thresholdType: toleranceType, + threshold: tolerance, + delta: this.delta, + cropImageA: clipRect, + cropImageB: clipRect, blockOut: this._blockOutAreas, verbose: this._args.verbose, }); - if (typeThreshold == ImageOptions.percent) { - valueThreshold = Math.floor(valueThreshold * 100); - console.log(`Using ${valueThreshold}\ ${typeThreshold} tolerance`); + if (toleranceType == ImageOptions.percent) { + if (tolerance >= 1) { + logError("Tolerance range is from 0 to 1 -> percentage thresholds: 1 = 100%, 0.2 = 20%"); + } + console.log(`Using ${tolerance * 100}% tolerance`); } else { - console.log(`Using ${valueThreshold} tolerance`); + console.log(`Using ${tolerance}px tolerance`); } const result = this.runDiff(diff, output); @@ -260,6 +360,17 @@ export class ImageHelper { public async clipRectangleImage(rect: IRectangle, path: string) { let imageToClip: PngJsImage; imageToClip = await this.readImage(path); + let shouldExit = false; + Object.getOwnPropertyNames(rect).forEach(prop => { + if (rect[prop] === undefined || rect[prop] === null) { + shouldExit = true; + return; + } + }); + if (shouldExit) { + logError(`Could not crop the image. Not enough data`, rect); + return + } imageToClip.clip(rect.x, rect.y, rect.width, rect.height); return new Promise((resolve, reject) => { imageToClip.writeImage(path, (err) => { @@ -282,4 +393,102 @@ export class ImageHelper { }); }) } + + private runDiff(diffOptions: BlinkDiff, diffImage: string) { + var that = this; + return new Promise((resolve, reject) => { + diffOptions.run(function (error, result) { + if (error) { + throw error; + } else { + const resultCode = diffOptions.hasPassed(result.code); + if (resultCode) { + console.log('Screen compare passed! Found ' + result.differences + ' differences.'); + return resolve(true); + } else { + const message = `Screen compare failed! Found ${result.differences} differences.\n`; + console.log(message); + if (Object.getOwnPropertyNames(that._args.testReporter).length > 0) { + that._args.testReporterLog(message); + if (that._args.testReporter.logImageTypes + && that._args.testReporter.logImageTypes.indexOf(LogImageType.everyImage) > -1) { + that._args.testReporterLog(`${basename(diffImage)} - ${message}`); + that._args.testReporterLog(diffImage); + } + } + return resolve(false); + } + } + }); + }); + } + + private increaseImageName(imageName: string, options: IImageCompareOptions) { + if (options.keepOriginalImageName) { + return imageName; + } + if (!imageName) { + logError(`Missing image name!`); + logError(`Consider to set + drive.imageHelper.testName + driver.imageHelper.options.imageName`); + throw new Error(`Missing image name!`) + } + if (this._imagesResults.size > 0) { + const images = new Array(); + this._imagesResults.forEach((v, k, map) => { + if (k.includes(imageName)) { + images.push(k); + } + }); + + images.sort((a, b) => { + const aNumber = +/\d+$/.exec(a); + const bNumber = +/\d+$/.exec(b); + + return bNumber - aNumber; + }); + if (images.length > 0) { + const lastImage = images[0]; + const number = /\d+$/.test(lastImage) ? +`${/\d+$/.exec(lastImage)}` + 1 : `2`; + imageName = `${imageName}_${number}`; + } + } + + return imageName; + } + + public extendOptions(options: IImageCompareOptions) { + options = options || {}; + Object.getOwnPropertyNames(this.options).forEach(prop => { + if (options[prop] === undefined || options[prop] === null) { + if (isObject(this.options[prop])) { + options[prop] = {}; + ImageHelper.fullClone(this.options[prop], options[prop]); + } else { + options[prop] = this.options[prop]; + } + } + }); + + if (!options.cropRectangle) { + ImageHelper.fullClone(this.imageCropRect, options.cropRectangle); + } + + return options; + } + + private static fullClone(src, target) { + Object.getOwnPropertyNames(src) + .forEach(prop => { + if (isObject(src[prop])) { + target[prop] = {}; + ImageHelper.fullClone(src[prop], target[prop]); + } else { + if (target[prop] === undefined || target[prop] === null) { + target[prop] = src[prop]; + } + } + }); + } } diff --git a/lib/interfaces/ns-capabilities-args.d.ts b/lib/interfaces/ns-capabilities-args.d.ts index 0a9101f..f1b29c9 100644 --- a/lib/interfaces/ns-capabilities-args.d.ts +++ b/lib/interfaces/ns-capabilities-args.d.ts @@ -43,4 +43,7 @@ export interface INsCapabilitiesArgs { driverConfig?: any; testReporter?: ITestReporter; logImageTypes?: Array; + storageByDeviceName?: string; + storageByPlatform?: string; + reportsPath?: string; } diff --git a/lib/interfaces/ns-capabilities-args.ts b/lib/interfaces/ns-capabilities-args.ts index 145a543..5e0ea58 100644 --- a/lib/interfaces/ns-capabilities-args.ts +++ b/lib/interfaces/ns-capabilities-args.ts @@ -44,4 +44,7 @@ export interface INsCapabilitiesArgs { driverConfig?: any; testReporter?: ITestReporter; logImageTypes?: Array; + storageByDeviceName?: string; + storageByPlatform?: string; + reportsPath?: string; } \ No newline at end of file diff --git a/lib/interfaces/rectangle.d.ts b/lib/interfaces/rectangle.d.ts index cfd3d0d..37f8dac 100644 --- a/lib/interfaces/rectangle.d.ts +++ b/lib/interfaces/rectangle.d.ts @@ -1,6 +1,6 @@ export interface IRectangle { - x: number; - y: number; - width: number; - height: number; + x?: number; + y?: number; + width?: number; + height?: number; } diff --git a/lib/interfaces/rectangle.ts b/lib/interfaces/rectangle.ts index 8eb0c9e..5895a52 100644 --- a/lib/interfaces/rectangle.ts +++ b/lib/interfaces/rectangle.ts @@ -1,6 +1,6 @@ export interface IRectangle { - x: number; - y: number; - width: number; - height: number; + x?: number; + y?: number; + width?: number; + height?: number; } \ No newline at end of file diff --git a/lib/ns-capabilities.d.ts b/lib/ns-capabilities.d.ts index cfecda9..7c94d5f 100644 --- a/lib/ns-capabilities.d.ts +++ b/lib/ns-capabilities.d.ts @@ -9,6 +9,9 @@ export declare class NsCapabilities implements INsCapabilities { private _parser; private _automationName; private _testReporter; + private _storageByDeviceName; + private _storageByPlatform; + private _reportsPath; projectDir: string; projectBinary: string; pluginRoot: string; @@ -50,19 +53,21 @@ export declare class NsCapabilities implements INsCapabilities { automationName: AutomationName; setAutomationNameFromString(automationName: String): void; /** - * Set testRoprter + * Set testReporter * @experimental */ /** - * Set testRoprter name like mochawesome - * Set testRoprter context usually this - * Set testRoprter log method like addContext in mochawesome + * Set testReporter name like mochawesome + * Set testReporter context usually this + * Set testReporter log method like addContext in mochawesome * @experimental */ testReporter: ITestReporter; + storageByDeviceName: string; + storageByPlatform: string; + readonly reportsPath: string; private _imagesReportDir; /** - * @exprimental * @param text to log in test report */ testReporterLog(text: any): any; diff --git a/lib/ns-capabilities.ts b/lib/ns-capabilities.ts index 5e79d6f..fd616a5 100644 --- a/lib/ns-capabilities.ts +++ b/lib/ns-capabilities.ts @@ -2,7 +2,7 @@ import { INsCapabilities } from "./interfaces/ns-capabilities"; import { INsCapabilitiesArgs } from "./interfaces/ns-capabilities-args"; import { AutomationName } from "./automation-name"; import { resolveCapabilities } from "./capabilities-helper"; -import { getAppPath, logInfo, logError, logWarn } from "./utils"; +import { getAppPath, logInfo, logError, logWarn, getStorageByDeviceName, getStorageByPlatform, getReportPath } from "./utils"; import { IDevice, Platform, Status, DeviceType } from "mobile-devices-controller"; import { IDeviceManager } from "./interfaces/device-manager"; import { existsSync, mkdirSync } from "fs"; @@ -14,6 +14,9 @@ import { LogImageType } from "./enums/log-image-type"; export class NsCapabilities implements INsCapabilities { private _automationName: AutomationName; private _testReporter: ITestReporter = {}; + private _storageByDeviceName: string; + private _storageByPlatform: string; + private _reportsPath: string; public projectDir: string; public projectBinary: string; @@ -96,7 +99,7 @@ export class NsCapabilities implements INsCapabilities { } /** - * Set testRoprter + * Set testReporter * @experimental */ public get testReporter() { @@ -104,9 +107,9 @@ export class NsCapabilities implements INsCapabilities { } /** - * Set testRoprter name like mochawesome - * Set testRoprter context usually this - * Set testRoprter log method like addContext in mochawesome + * Set testReporter name like mochawesome + * Set testReporter context usually this + * Set testReporter log method like addContext in mochawesome * @experimental */ public set testReporter(testReporter: ITestReporter) { @@ -116,9 +119,37 @@ export class NsCapabilities implements INsCapabilities { } } + get storageByDeviceName() { + if (!this._storageByDeviceName) { + this._storageByDeviceName = getStorageByDeviceName(this); + } + return this._storageByDeviceName; + } + + set storageByDeviceName(storageFullPath: string) { + this._storageByDeviceName = storageFullPath; + } + + get storageByPlatform() { + if (!this._storageByPlatform) { + this._storageByPlatform = getStorageByPlatform(this); + } + return this._storageByPlatform; + } + + set storageByPlatform(storageFullPath: string) { + this._storageByPlatform = storageFullPath; + } + + get reportsPath() { + if (!this._reportsPath) { + this._reportsPath = getReportPath(this); + } + return this._reportsPath; + } + private _imagesReportDir: string; /** - * @exprimental * @param text to log in test report */ public testReporterLog(text: any) { @@ -184,27 +215,40 @@ export class NsCapabilities implements INsCapabilities { searchQuery.status = Status.BOOTED; const runningDevices = await DeviceManager.getDevices(searchQuery); + if (runningDevices && runningDevices.length > 0) { + this.appiumCaps = this.appiumCaps || {}; const d = runningDevices[0]; - this.appiumCaps = { + const mandatoryAppiumCaps = { "platformName": d.platform, "noReset": true, "fullReset": false, "app": "" } + Object.getOwnPropertyNames(mandatoryAppiumCaps).forEach(prop => { + if (!this.appiumCaps[prop]) { + this.appiumCaps[prop] = mandatoryAppiumCaps[prop]; + } + }); + this.appiumCaps.deviceName = d.name; this.appiumCaps.platformVersion = d.apiLevel; this.appiumCaps.udid = d.token; + this.appiumCaps["newCommandTimeout"] = 999999; + if (this.deviceTypeOrPlatform === "android") { this.appiumCaps["lt"] = 60000; - this.appiumCaps["newCommandTimeout"] = 720; + this.appiumCaps["adbExecTimeout"] = 20000; + } else { + this.appiumCaps["wdaConnectionTimeout"] = 999999; } this.device = d; logInfo("Using device: ", d); + logInfo("appiumCaps: ", this.appiumCaps); } else { logError(`There is no running device of type:${this.deviceTypeOrPlatform}`); logInfo(`Use tns run ios/ android to install app on device!`) @@ -268,7 +312,7 @@ export class NsCapabilities implements INsCapabilities { } } else { if (this.isAndroid) { - if (this.tryGetAndroidApiLevel() > 6 || (this.appiumCaps["apiLevel"] && this.appiumCaps["apiLevel"].toLowerCase().includes("p"))) { + if (this.tryGetAndroidApiLevel() >= 6 || (this.appiumCaps["apiLevel"] && +this.appiumCaps["apiLevel"]) >= 23) { this.automationName = AutomationName.UiAutomator2; } } @@ -287,7 +331,7 @@ export class NsCapabilities implements INsCapabilities { try { if (this.appiumCaps["platformVersion"]) { const apiLevel = this.appiumCaps["platformVersion"].split(".").splice(0, 2).join('.'); - return parseFloat(apiLevel); + return +apiLevel; } } catch (error) { } return undefined; diff --git a/lib/parser.d.ts b/lib/parser.d.ts index f1be376..40edb2e 100644 --- a/lib/parser.d.ts +++ b/lib/parser.d.ts @@ -1,2 +1,2 @@ import { LogImageType } from "./enums/log-image-type"; -export declare const projectDir: string, projectBinary: string, pluginRoot: string, pluginBinary: string, port: number, verbose: boolean, appiumCapsLocation: string, testFolder: string, runType: string, isSauceLab: boolean, appPath: string, storage: string, testReports: string, devMode: boolean, ignoreDeviceController: boolean, wdaLocalPort: number, path: string, relaxedSecurity: boolean, cleanApp: boolean, attachToDebug: boolean, sessionId: string, startSession: boolean, capabilitiesName: string, imagesPath: string, startDeviceOptions: string, deviceTypeOrPlatform: string, device: any, driverConfig: any, logImageTypes: LogImageType[]; +export declare const projectDir: string, projectBinary: string, pluginRoot: string, pluginBinary: string, port: number, verbose: boolean, appiumCapsLocation: string, testFolder: string, runType: string, isSauceLab: boolean, appPath: string, storage: string, testReports: string, devMode: boolean, ignoreDeviceController: boolean, wdaLocalPort: number, path: string, relaxedSecurity: boolean, cleanApp: boolean, attachToDebug: boolean, sessionId: string, startSession: boolean, capabilitiesName: string, imagesPath: string, startDeviceOptions: string, deviceTypeOrPlatform: string, device: any, driverConfig: any, logImageTypes: LogImageType[], appiumCaps: any; diff --git a/lib/parser.ts b/lib/parser.ts index 0a280b9..c65a230 100644 --- a/lib/parser.ts +++ b/lib/parser.ts @@ -25,6 +25,7 @@ const config = (() => { describe: "Custom capabilities location `/some-path/appium.capabilities.json`", type: "string" }) + .option("appiumCaps", { alias: "caps", describe: "Apply additional appium capabilites" }) .option("capabilitiesName", { describe: "Capabilities file name", @@ -181,9 +182,12 @@ const config = (() => { deviceTypeOrPlatform: deviceTypeOrPlatform, device: options.device || process.env.npm_config_device, driverConfig: options.driverConfig, - logImageTypes: >options.logImageTypes + logImageTypes: >options.logImageTypes, + appiumCaps: options.appiumCaps }; + logWarn(`Parsed args: `, config); + return config; })(); @@ -216,5 +220,6 @@ export const { deviceTypeOrPlatform, device, driverConfig, - logImageTypes + logImageTypes, + appiumCaps }: INsCapabilitiesArgs = config; \ No newline at end of file diff --git a/lib/point.d.ts b/lib/point.d.ts index 92e92ab..40acdac 100644 --- a/lib/point.d.ts +++ b/lib/point.d.ts @@ -1,8 +1,6 @@ export declare class Point { - private _x; - private _y; - constructor(_x: number, _y: number); x: number; y: number; + constructor(x: number, y: number); toString(): string; } diff --git a/lib/point.ts b/lib/point.ts index af50529..fcc623a 100644 --- a/lib/point.ts +++ b/lib/point.ts @@ -1,21 +1,5 @@ export class Point { - constructor(private _x: number, private _y: number) { - } - - get x() { - return this._x; - } - - set x(x) { - this._x = x; - } - - get y() { - return this._y; - } - - set y(y) { - this._y = y; + constructor(public x: number, public y: number) { } public toString() { diff --git a/lib/ui-element.d.ts b/lib/ui-element.d.ts index f1949af..167d580 100644 --- a/lib/ui-element.d.ts +++ b/lib/ui-element.d.ts @@ -19,7 +19,10 @@ export declare class UIElement { tapCenter(): Promise; tapAtTheEnd(): Promise; /** + * @deprecated * Tap on element + * This method is not working very good with UiAutomator2 + * It is better to use click instead. */ tap(): Promise; /** diff --git a/lib/ui-element.ts b/lib/ui-element.ts index c92fb17..4b940e2 100644 --- a/lib/ui-element.ts +++ b/lib/ui-element.ts @@ -27,7 +27,7 @@ export class UIElement { public async tapCenter() { let action = new this._wd.TouchAction(this._driver); const rect = await this.getActualRectangle(); - this._args.testReporterLog(`Tap on center element ${{ "x": rect.x + rect.width / 2, "y": rect.y + rect.height / 2 }}`); + this._args.testReporterLog(`Tap on center element ${{ x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }}`); action .tap({ x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }); await action.perform(); @@ -44,8 +44,12 @@ export class UIElement { } /** + * @deprecated * Tap on element + * This method is not working very good with UiAutomator2 + * It is better to use click instead. */ + public async tap() { if (this._args.automationName == AutomationName.UiAutomator2) { return await this.tapCenter(); diff --git a/lib/utils.ts b/lib/utils.ts index 471ed5a..089de1c 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -218,6 +218,7 @@ export function isLinux() { const getDeviceName = (args) => { const deviceName = (args.attachToDebug || args.sessionId) ? args.device.name : args.appiumCaps.deviceName; + return deviceName; } @@ -227,6 +228,7 @@ export function getStorageByDeviceName(args: INsCapabilities) { const segments = args.imagesPath.split(/[\/\\]+/); storage = join(storage, segments.join(sep)); if (existsSync(storage)) { + logInfo(`Images storage set to: ${storage}!`); return storage; } else { const error = `Current imagesPath (${args.imagesPath}) does not exist !!!`; @@ -236,8 +238,11 @@ export function getStorageByDeviceName(args: INsCapabilities) { } const appName = resolveSauceLabAppName(getAppName(args)); storage = createStorageFolder(storage, appName); + storage = createStorageFolder(storage, getDeviceName(args)); + logWarn(`Images storage set to: ${storage}!`); + return storage; } @@ -247,6 +252,8 @@ export function getStorageByPlatform(args: INsCapabilities) { storage = createStorageFolder(storage, appName); storage = createStorageFolder(storage, args.appiumCaps.platformName.toLowerCase()); + logWarn(`Images storage set to: ${storage}!`); + return storage; } @@ -259,6 +266,8 @@ export const getStorage = (args: INsCapabilities) => { storage = createStorageFolder(storage, "images"); } + logWarn(`STORAGE: ${storage}`); + return storage; } @@ -505,7 +514,7 @@ export function getSessions(port, host = `0.0.0.0`) { http.get(`http://localhost:${port}/wd/hub/sessions`, (resp) => { let data = ''; - // A chunk of data has been recieved. + // A chunk of data has been received. resp.on('data', (chunk) => { data += chunk; }); @@ -586,15 +595,14 @@ export const prepareApp = async (args: INsCapabilities) => { } if (!args.ignoreDeviceController - && !args.attachToDebug - && !args.sessionId + && (args.attachToDebug || args.sessionId) && !args.appiumCaps[appPackage] && args.isIOS && args.appiumCaps.app) { - IOSController.getIOSPackageId(undefined, args.appiumCaps.app); + args.appiumCaps[appPackage] = IOSController.getIOSPackageId(undefined, args.appiumCaps.app); } - if (args.appiumCaps[appPackage]) { + if (args.appiumCaps[appPackage] && !args.appName) { const groupings = getRegexResultsAsArray(/(\w+)/gi, args.appiumCaps[appPackage]); args.appName = groupings[groupings.length - 1]; } diff --git a/package.json b/package.json index 4af37a0..dc72b5a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nativescript-dev-appium", - "version": "5.4.0", + "version": "6.0.0", "description": "A NativeScript plugin to help integrate and run Appium tests", "author": "NativeScript", "license": "MIT", @@ -38,8 +38,8 @@ "frame-comparer": "^2.0.1", "glob": "^7.1.0", "inquirer": "^6.2.0", - "mobile-devices-controller": "~4.0.1-5", - "wd": "~1.11.1", + "mobile-devices-controller": "^4.0.3-8", + "wd": "~1.11.3", "webdriverio": "~4.14.0", "yargs": "~12.0.5" }, From 032229f4ebe7c318c8ca6907f7bcbddb6b132dd5 Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Thu, 8 Aug 2019 23:12:30 +0300 Subject: [PATCH 11/51] fix: provide mandatory automation name for Android (#242) * release: cut the 6.0.0 release * fix: provide support for 'UiAutomator1' and set mandatory automation name * set default ios automation name XCUITest --- CHANGELOG.md | 17 +++++++++++++++++ lib/automation-name.d.ts | 1 + lib/automation-name.ts | 1 + lib/ns-capabilities.d.ts | 1 + lib/ns-capabilities.ts | 26 +++++++++++++++++++++----- 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0880b4f..3592494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ + +# [6.0.0](https://github.com/NativeScript/nativescript-dev-appium/compare/5.3.0...6.0.0) (2019-08-08) + + +### Bug Fixes + +* typos ([65c6aa3](https://github.com/NativeScript/nativescript-dev-appium/commit/65c6aa3)) +* **image-helper:** increase image name ([#237](https://github.com/NativeScript/nativescript-dev-appium/issues/237)) ([c075af8](https://github.com/NativeScript/nativescript-dev-appium/commit/c075af8)) + + +### Features + +* image-helper ([#236](https://github.com/NativeScript/nativescript-dev-appium/issues/236)) ([0d2c9fd](https://github.com/NativeScript/nativescript-dev-appium/commit/0d2c9fd)) +* resolve symlinked storages ([#235](https://github.com/NativeScript/nativescript-dev-appium/issues/235)) ([ef270c9](https://github.com/NativeScript/nativescript-dev-appium/commit/ef270c9)) + + + # [5.3.0](https://github.com/NativeScript/nativescript-dev-appium/compare/v5.2.0...v5.3.0) (2019-06-10) diff --git a/lib/automation-name.d.ts b/lib/automation-name.d.ts index e77c68f..d0270fa 100644 --- a/lib/automation-name.d.ts +++ b/lib/automation-name.d.ts @@ -1,5 +1,6 @@ export declare enum AutomationName { UiAutomator2 = "UIAutomator2", + UiAutomator1 = "UIAutomator1", Appium = "Appium", XCUITest = "XCUITest" } diff --git a/lib/automation-name.ts b/lib/automation-name.ts index 862156d..35e9eb7 100644 --- a/lib/automation-name.ts +++ b/lib/automation-name.ts @@ -1,5 +1,6 @@ export enum AutomationName { UiAutomator2 = "UIAutomator2", + UiAutomator1 = "UIAutomator1", Appium = "Appium", XCUITest = "XCUITest", } \ No newline at end of file diff --git a/lib/ns-capabilities.d.ts b/lib/ns-capabilities.d.ts index 7c94d5f..ea640cc 100644 --- a/lib/ns-capabilities.d.ts +++ b/lib/ns-capabilities.d.ts @@ -77,6 +77,7 @@ export declare class NsCapabilities implements INsCapabilities { shouldSetFullResetOption(): void; private setAutomationName; tryGetAndroidApiLevel(): number; + tryGetIOSApiLevel(): number; private resolveApplication; private checkMandatoryCapabilities; private throwExceptions; diff --git a/lib/ns-capabilities.ts b/lib/ns-capabilities.ts index fd616a5..1eaab31 100644 --- a/lib/ns-capabilities.ts +++ b/lib/ns-capabilities.ts @@ -309,11 +309,25 @@ export class NsCapabilities implements INsCapabilities { this.automationName = AutomationName.Appium; break; case AutomationName.XCUITest.toString().toLowerCase(): this.automationName = AutomationName.XCUITest; break; + case AutomationName.UiAutomator1.toString().toLowerCase(): + this.automationName = AutomationName.UiAutomator1; break; } } else { + const apiLevel = this.tryGetApiLevel(); if (this.isAndroid) { - if (this.tryGetAndroidApiLevel() >= 6 || (this.appiumCaps["apiLevel"] && +this.appiumCaps["apiLevel"]) >= 23) { + if ((apiLevel >= 6 && apiLevel <= 17) + || apiLevel >= 23) { this.automationName = AutomationName.UiAutomator2; + } else { + this.automationName = AutomationName.UiAutomator1; + } + } + + if (this.isIOS) { + if (apiLevel < 10) { + logWarn("Provide automationName") + } else { + this.automationName = AutomationName.XCUITest; } } } @@ -327,13 +341,15 @@ export class NsCapabilities implements INsCapabilities { } } - tryGetAndroidApiLevel() { + tryGetApiLevel() { try { - if (this.appiumCaps["platformVersion"]) { - const apiLevel = this.appiumCaps["platformVersion"].split(".").splice(0, 2).join('.'); - return +apiLevel; + const apiLevel = this.appiumCaps["platformVersion"] || this.appiumCaps["apiLevel"]; + if (this.isAndroid && apiLevel) { + return apiLevel.split(".").splice(0, 2).join('.'); } + return apiLevel; } catch (error) { } + return undefined; } From f04bfc85dcd875bf484c786505127cd353cdf336 Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Fri, 9 Aug 2019 15:39:52 +0300 Subject: [PATCH 12/51] release: cut the 6.0.0 release (#241) * release: cut the 6.0.0 release * release: cut the 6.0.0 release --- CHANGELOG.md | 47 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3592494..bf4cad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,47 @@ # [6.0.0](https://github.com/NativeScript/nativescript-dev-appium/compare/5.3.0...6.0.0) (2019-08-08) -### Bug Fixes +### Features -* typos ([65c6aa3](https://github.com/NativeScript/nativescript-dev-appium/commit/65c6aa3)) -* **image-helper:** increase image name ([#237](https://github.com/NativeScript/nativescript-dev-appium/issues/237)) ([c075af8](https://github.com/NativeScript/nativescript-dev-appium/commit/c075af8)) +* driver.getOrientation() +* driver.setOrientation(orientation: DeviceOrientation) +* restartApp() +* image-helper ([#236](https://github.com/NativeScript/nativescript-dev-appium/issues/236)) ([0d2c9fd](https://github.com/NativeScript/nativescript-dev-appium/commit/0d2c9fd)) +* image-helper set counter to images ([#237](https://github.com/NativeScript/nativescript-dev-appium/issues/237)) ([c075af8](https://github.com/NativeScript/nativescript-dev-appium/commit/c075af8)) +* resolve sym-linked storages ([#235](https://github.com/NativeScript/nativescript-dev-appium/issues/235)) ([ef270c9](https://github.com/NativeScript/nativescript-dev-appium/commit/ef270c9) -### Features +### BREAKING CHANGES: -* image-helper ([#236](https://github.com/NativeScript/nativescript-dev-appium/issues/236)) ([0d2c9fd](https://github.com/NativeScript/nativescript-dev-appium/commit/0d2c9fd)) -* resolve symlinked storages ([#235](https://github.com/NativeScript/nativescript-dev-appium/issues/235)) ([ef270c9](https://github.com/NativeScript/nativescript-dev-appium/commit/ef270c9)) +nativescript-dev-appium already takes in account viewportRect which is provided by appium(1.10.0 and above). The view port rectangle will be used when the images are compared. Custom view port could be provided using: + +`appium capabilities : "viewportRect": {}` + +`driver.imageHelper.options.cropRectangle` + +`imageHelper.compareScreen({cropRectangle : {x, y, wight, height}});` + +#### Default image comparison tolerance +Before: +`1%` +Now: +`0%` + +driver.scrollTo() accepts IRectangle +Before: + +`scrollTo(direction: Direction, element: any, startPoint: Point, yOffset: number, xOffset?: number, retryCount?: number)` +Now: + +`scrollTo(direction: Direction, element: any, startPoint: Point, offsetPoint: Point, retryCount?: number): Promise;` + +#### In order to use platform specific images: +Before: + +automatically +Now: + +`driver.imageHelper.options.isDeviceSpecific = false;` @@ -22,9 +53,9 @@ ### Bug Fixes * mochawesome reporter for windows ([#222](https://github.com/NativeScript/nativescript-dev-appium/issues/222)) ([bd612a6](https://github.com/NativeScript/nativescript-dev-appium/commit/bd612a6)) -* skip propmting on post install when the console is not interactive ([1042328](https://github.com/NativeScript/nativescript-dev-appium/commit/1042328)) +* skip prompting on post install when the console is not interactive ([1042328](https://github.com/NativeScript/nativescript-dev-appium/commit/1042328)) * **device-controller:** token in server scenario ([c847536](https://github.com/NativeScript/nativescript-dev-appium/commit/c847536)) -* undifined token ([52ec166](https://github.com/NativeScript/nativescript-dev-appium/commit/52ec166)) +* undefined token ([52ec166](https://github.com/NativeScript/nativescript-dev-appium/commit/52ec166)) ### Features From ec2c5586a524531ac382d115c64030d9ced924bc Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Tue, 13 Aug 2019 17:24:03 +0300 Subject: [PATCH 13/51] fix: remove duplicated property (#243) * fix: remove duplicated property * chore: setOrientation --- lib/appium-driver.ts | 34 +++++++++++--- lib/image-helper.d.ts | 4 +- lib/image-helper.ts | 92 ++++++++++++++++++------------------- lib/ns-capabilities.d.ts | 5 +- lib/ns-capabilities.ts | 30 ++++++------ test/device-manager.spec.ts | 4 +- 6 files changed, 91 insertions(+), 78 deletions(-) diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index 197114e..f3832a3 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -48,6 +48,7 @@ import { LogType } from "./log-types"; import { screencapture } from "./helpers/screenshot-manager"; import { LogImageType } from "./enums/log-image-type"; import { DeviceOrientation } from "./enums/device-orientation"; +import { NsCapabilities } from "./ns-capabilities"; export class AppiumDriver { private _defaultWaitTime: number = 5000; @@ -546,13 +547,32 @@ export class AppiumDriver { logInfo(`Set device orientation: ${orientation}`) await this._driver.setOrientation(orientation); - if (orientation === DeviceOrientation.LANDSCAPE) { - this.imageHelper.imageCropRect.x = this._imageHelper.options.cropRectangle.x; - this.imageHelper.imageCropRect.y = this._imageHelper.options.cropRectangle.y; - this.imageHelper.imageCropRect.width = this._imageHelper.options.cropRectangle.height; - this.imageHelper.imageCropRect.height = this._imageHelper.options.cropRectangle.width; - } else { - this.imageHelper.imageCropRect = undefined; + if (orientation === DeviceOrientation.LANDSCAPE && this.isAndroid) { + if ((this.nsCapabilities).tryGetApiLevel() < 6.0) { + // HACK since the image is rotated and action bar is on the bottom of the image, it is needed to exclude action bar from bottom. + const height = this._imageHelper.options.cropRectangle.width - this._imageHelper.options.cropRectangle.y; + const width = this._imageHelper.options.cropRectangle.height + this._imageHelper.options.cropRectangle.y; + this.imageHelper.options.cropRectangle.y = 0; + this.imageHelper.options.cropRectangle.width = width; + this.imageHelper.options.cropRectangle.height = height; + } else if ((this.nsCapabilities).tryGetApiLevel() >= 6.0) { + const height = this._imageHelper.options.cropRectangle.width - this.imageHelper.options.cropRectangle.y; + const width = this._imageHelper.options.cropRectangle.height + this.imageHelper.options.cropRectangle.y; + this.imageHelper.options.cropRectangle.width = width; + this.imageHelper.options.cropRectangle.height = height; + } + } + else if (orientation === DeviceOrientation.LANDSCAPE && this.isIOS) { + this.imageHelper.options.cropRectangle.x = 0; + const height = this._imageHelper.options.cropRectangle.width; + const width = this._imageHelper.options.cropRectangle.height + this._imageHelper.options.cropRectangle.y; + this.imageHelper.options.cropRectangle.y = 0; + + this.imageHelper.options.cropRectangle.width = width; + this.imageHelper.options.cropRectangle.height = height; + } + else { + this.imageHelper.resetDefaultOptions(); } } diff --git a/lib/image-helper.d.ts b/lib/image-helper.d.ts index e29a86f..f499009 100644 --- a/lib/image-helper.d.ts +++ b/lib/image-helper.d.ts @@ -63,7 +63,6 @@ export declare class ImageHelper { private _driver; private _blockOutAreas; private _imagesResults; - private _imageCropRect; private _options; private _defaultOptions; constructor(_args: INsCapabilities, _driver: AppiumDriver); @@ -80,7 +79,6 @@ export declare class ImageHelper { */ delta: number; options: IImageCompareOptions; - imageCropRect: IRectangle; blockOutAreas: IRectangle[]; compareScreen(options?: IImageCompareOptions): Promise; compareElement(element: UIElement, options?: IImageCompareOptions): Promise; @@ -97,7 +95,7 @@ export declare class ImageHelper { getExpectedImagePathByDevice(imageName: string): string; getExpectedImagePathByPlatform(imageName: string): string; compare(options: IImageCompareOptions): Promise; - compareImages(actual: string, expected: string, output: string, tolerance: number, toleranceType: ImageOptions): Promise; + compareImages(options: IImageCompareOptions, actual: string, expected: string, output: string): Promise; clipRectangleImage(rect: IRectangle, path: string): Promise<{}>; readImage(path: string): Promise; private runDiff; diff --git a/lib/image-helper.ts b/lib/image-helper.ts index 9e5b968..6213a05 100644 --- a/lib/image-helper.ts +++ b/lib/image-helper.ts @@ -9,7 +9,7 @@ import { AppiumDriver } from "./appium-driver"; import { logError, checkImageLogType, resolvePath, copy, addExt, logWarn } from "./utils"; import { unlinkSync, existsSync, mkdirSync } from "fs"; import { basename, join } from "path"; -import { isObject } from "util"; +import { isObject, isNumber } from "util"; import { logInfo } from "mobile-devices-controller/lib/utils"; export interface IImageCompareOptions { @@ -82,7 +82,6 @@ export interface IImageCompareOptions { export class ImageHelper { private _blockOutAreas: IRectangle[]; private _imagesResults = new Map(); - private _imageCropRect: IRectangle; private _options: IImageCompareOptions = {}; private _defaultOptions: IImageCompareOptions = { timeOutSeconds: 2, @@ -101,12 +100,14 @@ export class ImageHelper { constructor(private _args: INsCapabilities, private _driver: AppiumDriver) { this._defaultOptions.cropRectangle = (this._args.appiumCaps && this._args.appiumCaps.viewportRect) || this._args.device.viewportRect; if (!this._defaultOptions.cropRectangle - || this._defaultOptions.cropRectangle.y === undefined - || this._defaultOptions.cropRectangle.y === null - || this._defaultOptions.cropRectangle.y === NaN) { + || !isNumber(this._defaultOptions.cropRectangle.y)) { this._defaultOptions.cropRectangle = this._defaultOptions.cropRectangle || {}; this._defaultOptions.cropRectangle.y = this._args.device.config.offsetPixels || 0; this._defaultOptions.cropRectangle.x = 0; + if (this._args.device.deviceScreenSize && this._args.device.deviceScreenSize.width && this._args.device.deviceScreenSize.height) { + this._defaultOptions.cropRectangle.height = this._args.device.deviceScreenSize.height - this._defaultOptions.cropRectangle.y; + this._defaultOptions.cropRectangle.width = this._args.device.deviceScreenSize.width - this._defaultOptions.cropRectangle.x; + } } ImageHelper.fullClone(this._defaultOptions, this._options); @@ -137,14 +138,6 @@ export class ImageHelper { this._options = this.extendOptions(options); } - get imageCropRect(): IRectangle { - return this._imageCropRect || this.options.cropRectangle; - } - - set imageCropRect(clipRectangle: IRectangle) { - this._imageCropRect = clipRectangle; - } - get blockOutAreas() { return this._blockOutAreas; } @@ -257,7 +250,6 @@ export class ImageHelper { // First time capture if (!existsSync(pathExpectedImage)) { await captureFirstImage(); - return false; } @@ -269,7 +261,7 @@ export class ImageHelper { const pathDiffImage = pathActualImage.replace("actual", "diff"); // await this.prepareImageToCompare(pathActualImage, options.cropRectangle); - let result = await this.compareImages(pathActualImage, pathExpectedImage, pathDiffImage, options.tolerance, options.toleranceType); + let result = await this.compareImages(options, pathActualImage, pathExpectedImage, pathDiffImage); // Iterate if (!result) { @@ -283,7 +275,7 @@ export class ImageHelper { await this.clipRectangleImage(options.cropRectangle, pathActualImage); } // await this.prepareImageToCompare(pathActualImage, this.imageCropRect); - result = await this.compareImages(pathActualImage, pathExpectedImage, pathDiffImage, options.tolerance, options.toleranceType); + result = await this.compareImages(options, pathActualImage, pathExpectedImage, pathDiffImage); if (!result && checkImageLogType(this._args.testReporter, LogImageType.everyImage)) { this._args.testReporterLog(`Actual image: ${basename(pathActualImage).replace(/\.\w{3,3}$/ig, "")}`); this._args.testReporterLog(join(this._args.reportsPath, basename(pathActualImage))); @@ -314,12 +306,12 @@ export class ImageHelper { return result; } - public compareImages(actual: string, expected: string, output: string, tolerance: number, toleranceType: ImageOptions) { + public compareImages(options: IImageCompareOptions, actual: string, expected: string, output: string) { const clipRect = { - x: this.imageCropRect.x, - y: this.imageCropRect.y, - width: this.imageCropRect.width, - height: this.imageCropRect.height + x: this.options.cropRectangle.x, + y: this.options.cropRectangle.y, + width: this.options.cropRectangle.width, + height: this.options.cropRectangle.height } if (!this.options.keepOriginalImageSize) { @@ -334,8 +326,8 @@ export class ImageHelper { imageBPath: expected, imageOutputPath: output, imageOutputLimit: this.imageOutputLimit, - thresholdType: toleranceType, - threshold: tolerance, + thresholdType: options.toleranceType, + threshold: options.tolerance, delta: this.delta, cropImageA: clipRect, cropImageB: clipRect, @@ -343,13 +335,13 @@ export class ImageHelper { verbose: this._args.verbose, }); - if (toleranceType == ImageOptions.percent) { - if (tolerance >= 1) { + if (options.toleranceType == ImageOptions.percent) { + if (options.tolerance >= 1) { logError("Tolerance range is from 0 to 1 -> percentage thresholds: 1 = 100%, 0.2 = 20%"); } - console.log(`Using ${tolerance * 100}% tolerance`); + console.log(`Using ${options.tolerance * 100}% tolerance`); } else { - console.log(`Using ${tolerance}px tolerance`); + console.log(`Using ${options.tolerance}px tolerance`); } const result = this.runDiff(diff, output); @@ -361,26 +353,34 @@ export class ImageHelper { let imageToClip: PngJsImage; imageToClip = await this.readImage(path); let shouldExit = false; - Object.getOwnPropertyNames(rect).forEach(prop => { - if (rect[prop] === undefined || rect[prop] === null) { - shouldExit = true; - return; - } - }); + if (!isNumber(rect["x"]) + || !isNumber(rect["y"]) + || !isNumber(rect["width"]) + || !isNumber(rect["height"])) { + shouldExit = true; + } if (shouldExit) { - logError(`Could not crop the image. Not enough data`, rect); - return + logError(`Could not crop the image. Not enough data {x: ${rect["x"]}, y: ${rect["y"]}, width: ${rect["width"]}, height: ${rect["height"]}}`); } - imageToClip.clip(rect.x, rect.y, rect.width, rect.height); - return new Promise((resolve, reject) => { - imageToClip.writeImage(path, (err) => { - if (err) { - return reject(err); - } - return resolve(); - }); - }) + if (!shouldExit) { + imageToClip.clip(rect.x, rect.y, rect.width, rect.height); + } else { + logWarn("Image will not be cropped!") + return true; + } + return new Promise((resolve, reject) => { + try { + imageToClip.writeImage(path, (err) => { + if (err) { + return reject(err); + } + return resolve(); + }); + } catch (error) { + logError(error); + } + }); } public readImage(path: string): Promise { @@ -471,10 +471,6 @@ export class ImageHelper { } }); - if (!options.cropRectangle) { - ImageHelper.fullClone(this.imageCropRect, options.cropRectangle); - } - return options; } diff --git a/lib/ns-capabilities.d.ts b/lib/ns-capabilities.d.ts index ea640cc..ae4f03a 100644 --- a/lib/ns-capabilities.d.ts +++ b/lib/ns-capabilities.d.ts @@ -74,10 +74,9 @@ export declare class NsCapabilities implements INsCapabilities { extend(args: INsCapabilities): this; validateArgs(): Promise; private isAndroidPlatform; - shouldSetFullResetOption(): void; + setResetOption(): void; + tryGetApiLevel(): number; private setAutomationName; - tryGetAndroidApiLevel(): number; - tryGetIOSApiLevel(): number; private resolveApplication; private checkMandatoryCapabilities; private throwExceptions; diff --git a/lib/ns-capabilities.ts b/lib/ns-capabilities.ts index 1eaab31..cb99c78 100644 --- a/lib/ns-capabilities.ts +++ b/lib/ns-capabilities.ts @@ -263,7 +263,7 @@ export class NsCapabilities implements INsCapabilities { this.resolveApplication(); this.checkMandatoryCapabilities(); this.throwExceptions(); - this.shouldSetFullResetOption(); + this.setResetOption(); this.isValidated = true; } else { @@ -275,7 +275,7 @@ export class NsCapabilities implements INsCapabilities { return this.appiumCaps && this.appiumCaps ? this.appiumCaps.platformName.toLowerCase().includes("android") : undefined; } - public shouldSetFullResetOption() { + public setResetOption() { if (this.attachToDebug || this.devMode) { this.appiumCaps["fullReset"] = false; this.appiumCaps["noReset"] = true; @@ -300,6 +300,18 @@ export class NsCapabilities implements INsCapabilities { } } + public tryGetApiLevel() { + try { + const apiLevel = this.appiumCaps["platformVersion"] || this.appiumCaps["apiLevel"]; + if (this.isAndroid && apiLevel) { + return +apiLevel.split(".").splice(0, 2).join('.'); + } + return +apiLevel; + } catch (error) { } + + return undefined; + } + private setAutomationName() { if (this.appiumCaps["automationName"]) { switch (this.appiumCaps["automationName"].toLowerCase()) { @@ -313,7 +325,7 @@ export class NsCapabilities implements INsCapabilities { this.automationName = AutomationName.UiAutomator1; break; } } else { - const apiLevel = this.tryGetApiLevel(); + const apiLevel = +this.tryGetApiLevel(); if (this.isAndroid) { if ((apiLevel >= 6 && apiLevel <= 17) || apiLevel >= 23) { @@ -341,18 +353,6 @@ export class NsCapabilities implements INsCapabilities { } } - tryGetApiLevel() { - try { - const apiLevel = this.appiumCaps["platformVersion"] || this.appiumCaps["apiLevel"]; - if (this.isAndroid && apiLevel) { - return apiLevel.split(".").splice(0, 2).join('.'); - } - return apiLevel; - } catch (error) { } - - return undefined; - } - private resolveApplication() { if (this.isSauceLab) { if (this.appPath) { diff --git a/test/device-manager.spec.ts b/test/device-manager.spec.ts index a44153e..88a9bef 100644 --- a/test/device-manager.spec.ts +++ b/test/device-manager.spec.ts @@ -83,7 +83,7 @@ describe("ios-devices", () => { deviceManager = new DeviceManager(); appiumArgs = new NsCapabilities({}); appiumArgs.extend({ appiumCaps: { platformName: Platform.IOS, fullReset: false, deviceName: /iPhone X/ } }) - appiumArgs.shouldSetFullResetOption(); + appiumArgs.setResetOption(); }); after("Kill all simulators", () => { @@ -101,7 +101,7 @@ describe("ios-devices", () => { it("Start simulator fullReset: true, should kill device", async () => { appiumArgs.extend({ appiumCaps: { platformName: Platform.IOS, fullReset: true, deviceName: /iPhone X/ } }); - appiumArgs.shouldSetFullResetOption(); + appiumArgs.setResetOption(); const device = await deviceManager.startDevice(appiumArgs); let foundBootedDevices = await DeviceController.getDevices({ platform: Platform.IOS, status: Status.BOOTED }); assert.isTrue(foundBootedDevices.some(d => d.token === device.token)); From 620a9e8eb0b5e970c532e5c7a245fe2ab333d7e2 Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Tue, 13 Aug 2019 21:15:55 +0300 Subject: [PATCH 14/51] fix(setOrientation): calc view port according to automation name (#244) --- lib/appium-driver.ts | 6 ++++-- lib/parser.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index f3832a3..ab1f9d7 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -49,6 +49,7 @@ import { screencapture } from "./helpers/screenshot-manager"; import { LogImageType } from "./enums/log-image-type"; import { DeviceOrientation } from "./enums/device-orientation"; import { NsCapabilities } from "./ns-capabilities"; +import { AutomationName } from "./automation-name"; export class AppiumDriver { private _defaultWaitTime: number = 5000; @@ -548,14 +549,15 @@ export class AppiumDriver { await this._driver.setOrientation(orientation); if (orientation === DeviceOrientation.LANDSCAPE && this.isAndroid) { - if ((this.nsCapabilities).tryGetApiLevel() < 6.0) { + if ((this.nsCapabilities).automationName === AutomationName.UiAutomator1 + || (this.nsCapabilities).automationName === AutomationName.Appium) { // HACK since the image is rotated and action bar is on the bottom of the image, it is needed to exclude action bar from bottom. const height = this._imageHelper.options.cropRectangle.width - this._imageHelper.options.cropRectangle.y; const width = this._imageHelper.options.cropRectangle.height + this._imageHelper.options.cropRectangle.y; this.imageHelper.options.cropRectangle.y = 0; this.imageHelper.options.cropRectangle.width = width; this.imageHelper.options.cropRectangle.height = height; - } else if ((this.nsCapabilities).tryGetApiLevel() >= 6.0) { + } else if ((this.nsCapabilities).automationName === AutomationName.UiAutomator2) { const height = this._imageHelper.options.cropRectangle.width - this.imageHelper.options.cropRectangle.y; const width = this._imageHelper.options.cropRectangle.height + this.imageHelper.options.cropRectangle.y; this.imageHelper.options.cropRectangle.width = width; diff --git a/lib/parser.ts b/lib/parser.ts index c65a230..977e173 100644 --- a/lib/parser.ts +++ b/lib/parser.ts @@ -25,7 +25,7 @@ const config = (() => { describe: "Custom capabilities location `/some-path/appium.capabilities.json`", type: "string" }) - .option("appiumCaps", { alias: "caps", describe: "Apply additional appium capabilites" }) + .option("appiumCaps", { alias: "caps", describe: "Apply additional appium capabilities" }) .option("capabilitiesName", { describe: "Capabilities file name", From bae9e6f7a03b3312f9292e0af2a7d40e520bae2d Mon Sep 17 00:00:00 2001 From: Zdravko Date: Wed, 14 Aug 2019 15:57:54 +0300 Subject: [PATCH 15/51] chore: output image on compare. (#245) This improves readability when multiple images are compared in single test --- lib/image-helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/image-helper.ts b/lib/image-helper.ts index 6213a05..7ead1ae 100644 --- a/lib/image-helper.ts +++ b/lib/image-helper.ts @@ -259,7 +259,7 @@ export class ImageHelper { await this.clipRectangleImage(options.cropRectangle, pathActualImage); } const pathDiffImage = pathActualImage.replace("actual", "diff"); - + console.log(`\n Comparing ${pathExpectedImage}`); // await this.prepareImageToCompare(pathActualImage, options.cropRectangle); let result = await this.compareImages(options, pathActualImage, pathExpectedImage, pathDiffImage); From 78f4c0049e9ee4ae59575a916c3d1f8bb127b971 Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Thu, 15 Aug 2019 13:17:43 +0300 Subject: [PATCH 16/51] chore: update mobile-devices-controller (#246) --- index.ts | 2 +- lib/device-manager.d.ts | 2 +- lib/device-manager.ts | 10 +++++----- lib/parser.d.ts | 2 +- lib/utils.ts | 2 +- package.json | 4 ++-- test/device-manager.spec.ts | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/index.ts b/index.ts index f964291..2f07e08 100644 --- a/index.ts +++ b/index.ts @@ -75,7 +75,7 @@ export async function stopServer() { } if (nsCapabilities.cleanApp && !nsCapabilities.ignoreDeviceController) { - await DeviceController.uninstallApp(nsCapabilities.device, nsCapabilities.appPath); + await DeviceController.uninstallApplication(nsCapabilities.device, nsCapabilities.appPath); logInfo("Application from device is uninstalled.") } }; diff --git a/lib/device-manager.d.ts b/lib/device-manager.d.ts index f0bdbe6..e535392 100644 --- a/lib/device-manager.d.ts +++ b/lib/device-manager.d.ts @@ -13,7 +13,7 @@ export declare class DeviceManager implements IDeviceManager { static getInstalledApps(device: IDevice): Promise; static getDefaultDevice(args: INsCapabilities, deviceName?: string, token?: string, type?: DeviceType, platformVersion?: number): IDevice; private static convertViewportRectToIRectangle; - static applyAppiumSessionInfoDetails(args: INsCapabilities, sessionInfoDetails: any): any; + static applyAppiumSessionInfoDetails(args: INsCapabilities, sessionInfoDetails: any): IDevice; static setDontKeepActivities(args: INsCapabilities, driver: any, value: any): Promise; static executeShellCommand(driver: any, commandArgs: { command: string; diff --git a/lib/device-manager.ts b/lib/device-manager.ts index 148f7be..96e68ab 100644 --- a/lib/device-manager.ts +++ b/lib/device-manager.ts @@ -140,18 +140,18 @@ export class DeviceManager implements IDeviceManager { public async installApp(args: INsCapabilities): Promise { if (args.isIOS) { - IOSController.installApp(args.device, args.appiumCaps.app); + IOSController.uninstallApplication(args.device, args.appiumCaps.app); console.log(`Application is successfully installed!`) } else { - AndroidController.installApp(args.device, args.appiumCaps.app) + AndroidController.uninstallApplication(args.device, args.appiumCaps.app) } } public async uninstallApp(args: INsCapabilities): Promise { if (args.isIOS) { - await IOSController.uninstallApp(args.device, args.appPath, args.appiumCaps["bundleId"]); + await IOSController.uninstallApplication(args.device, args.appPath, args.appiumCaps["bundleId"]); } else { - await Promise.resolve(AndroidController.uninstallApp(args.device, args.appiumCaps["appPackage"])); + await Promise.resolve(AndroidController.uninstallApplication(args.device, args.appiumCaps["appPackage"])); } } @@ -301,7 +301,7 @@ export class DeviceManager implements IDeviceManager { } public getPackageId(device: IDevice, appPath: string): string { - const appActivity = (device.type === DeviceType.EMULATOR || device.platform === Platform.ANDROID) ? AndroidController.getPackageId(appPath) : IOSController.getIOSPackageId(device.type, appPath); + const appActivity = (device.type === DeviceType.EMULATOR || device.platform === Platform.ANDROID) ? AndroidController.getPackageId(appPath) : IOSController.getBundleId(device.type, appPath); return appActivity; } diff --git a/lib/parser.d.ts b/lib/parser.d.ts index 40edb2e..386ae5d 100644 --- a/lib/parser.d.ts +++ b/lib/parser.d.ts @@ -1,2 +1,2 @@ import { LogImageType } from "./enums/log-image-type"; -export declare const projectDir: string, projectBinary: string, pluginRoot: string, pluginBinary: string, port: number, verbose: boolean, appiumCapsLocation: string, testFolder: string, runType: string, isSauceLab: boolean, appPath: string, storage: string, testReports: string, devMode: boolean, ignoreDeviceController: boolean, wdaLocalPort: number, path: string, relaxedSecurity: boolean, cleanApp: boolean, attachToDebug: boolean, sessionId: string, startSession: boolean, capabilitiesName: string, imagesPath: string, startDeviceOptions: string, deviceTypeOrPlatform: string, device: any, driverConfig: any, logImageTypes: LogImageType[], appiumCaps: any; +export declare const projectDir: string, projectBinary: string, pluginRoot: string, pluginBinary: string, port: number, verbose: boolean, appiumCapsLocation: string, testFolder: string, runType: string, isSauceLab: boolean, appPath: string, storage: string, testReports: string, devMode: boolean, ignoreDeviceController: boolean, wdaLocalPort: number, path: string, relaxedSecurity: boolean, cleanApp: boolean, attachToDebug: boolean, sessionId: string, startSession: boolean, capabilitiesName: string, imagesPath: string, startDeviceOptions: string, deviceTypeOrPlatform: string, device: import("mobile-devices-controller/lib/device").IDevice, driverConfig: any, logImageTypes: LogImageType[], appiumCaps: any; diff --git a/lib/utils.ts b/lib/utils.ts index 089de1c..7df007e 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -599,7 +599,7 @@ export const prepareApp = async (args: INsCapabilities) => { && !args.appiumCaps[appPackage] && args.isIOS && args.appiumCaps.app) { - args.appiumCaps[appPackage] = IOSController.getIOSPackageId(undefined, args.appiumCaps.app); + args.appiumCaps[appPackage] = IOSController.getBundleId(undefined, args.appiumCaps.app); } if (args.appiumCaps[appPackage] && !args.appName) { diff --git a/package.json b/package.json index dc72b5a..b1ce790 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "frame-comparer": "^2.0.1", "glob": "^7.1.0", "inquirer": "^6.2.0", - "mobile-devices-controller": "^4.0.3-8", + "mobile-devices-controller": "~5.0.0", "wd": "~1.11.3", "webdriverio": "~4.14.0", "yargs": "~12.0.5" @@ -58,4 +58,4 @@ "test": "mocha --timeout 999999", "version": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md" } -} +} \ No newline at end of file diff --git a/test/device-manager.spec.ts b/test/device-manager.spec.ts index 88a9bef..bec3568 100644 --- a/test/device-manager.spec.ts +++ b/test/device-manager.spec.ts @@ -260,7 +260,7 @@ describe("start-device-by-apiLevel", async () => { await server.start(nsCaps.port); const driver = await AppiumDriver.createAppiumDriver(nsCaps); - const apps = IOSController.getInstalledApps(nsCaps.device); + const apps = IOSController.getInstalledApplications(nsCaps.device); const isInstalled = apps.some(app => app.includes(nsCaps.appiumCaps.bundleId)); assert.isTrue(isInstalled); From 8e958dc00bd3046978140b76382a68bc90ef7b07 Mon Sep 17 00:00:00 2001 From: Zdravko Date: Fri, 16 Aug 2019 01:55:37 +0300 Subject: [PATCH 17/51] fix: apply provided capabilities for density and offset in sauceLabs runs (#247) --- lib/device-manager.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/device-manager.ts b/lib/device-manager.ts index 96e68ab..604e67a 100644 --- a/lib/device-manager.ts +++ b/lib/device-manager.ts @@ -11,7 +11,7 @@ import { DeviceType, sortDescByApiLevelPredicate } from "mobile-devices-controller"; -import { isRegExp } from "util"; +import { isRegExp, isNumber } from "util"; import { NsCapabilities } from "./ns-capabilities"; export class DeviceManager implements IDeviceManager { @@ -201,13 +201,14 @@ export class DeviceManager implements IDeviceManager { args.device.apiLevel = sessionInfoDetails.deviceApiLevel; args.device.deviceScreenDensity = sessionInfoDetails.deviceScreenDensity / 100; - args.device.config = { "density": args.device.deviceScreenDensity, "offsetPixels": +sessionInfoDetails.statBarHeight }; + args.device.config = { "density": args.device.deviceScreenDensity || args.device.config.density, "offsetPixels": +sessionInfoDetails.statBarHeight || args.device.config.offsetPixels }; } else { args.device.apiLevel = sessionInfoDetails.platformVersion; args.device.deviceScreenDensity = sessionInfoDetails.pixelRatio; - args.device.config = { "density": sessionInfoDetails.pixelRatio, "offsetPixels": +sessionInfoDetails.viewportRect.top - +sessionInfoDetails.statBarHeight }; + const offsetPixels = +sessionInfoDetails.viewportRect.top - +sessionInfoDetails.statBarHeight; + args.device.config = { "density": sessionInfoDetails.pixelRatio || args.device.config.density, "offsetPixels": isNumber(offsetPixels) ? offsetPixels : args.device.config.offsetPixels }; } - + args.device.statBarHeight = sessionInfoDetails.statBarHeight; args.device.viewportRect = DeviceManager.convertViewportRectToIRectangle(sessionInfoDetails.viewportRect); From 7b2c8b850b987dab3491ca9a98534924a0f9a270 Mon Sep 17 00:00:00 2001 From: Zdravko Date: Fri, 16 Aug 2019 13:46:58 +0300 Subject: [PATCH 18/51] fix: xml source generation (#249) --- lib/appium-driver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index ab1f9d7..fabc4a2 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -579,7 +579,7 @@ export class AppiumDriver { } public async source() { - return await this._webio.source(); + return await this._driver.source(); } public async sessionId() { @@ -714,7 +714,7 @@ export class AppiumDriver { const path = resolvePath(this._args.reportsPath, fileName); const xml = await this.source(); - writeFileSync(path, xml.value, 'utf8'); + writeFileSync(path, xml, 'utf8'); } public async logDeviceLog(fileName, logType: LogType, filter: string = undefined) { From a997bd735ab9c143bc94f9a0ac547a6396f09142 Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Mon, 19 Aug 2019 14:00:35 +0300 Subject: [PATCH 19/51] release: cut the 6.0.0 release (#248) * release: cut the 6.0.0 release * release: cut the 6.0.0 release * release: cut the 6.0.0 release * release: cut the 6.0.0 release * chore: fix title --- CHANGELOG.md | 9 ++++++++- package.json | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf4cad3..f60915b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ # [6.0.0](https://github.com/NativeScript/nativescript-dev-appium/compare/5.3.0...6.0.0) (2019-08-08) +### Bug Fixes + +* **image-helper:** set image name counter ([#237](https://github.com/NativeScript/nativescript-dev-appium/issues/237)) ([c075af8](https://github.com/NativeScript/nativescript-dev-appium/commit/c075af8)) +* provide mandatory automation name for Android ([#242](https://github.com/NativeScript/nativescript-dev-appium/issues/242)) ([032229f](https://github.com/NativeScript/nativescript-dev-appium/commit/032229f)) +* remove duplicated property ([#243](https://github.com/NativeScript/nativescript-dev-appium/issues/243)) ([ec2c558](https://github.com/NativeScript/nativescript-dev-appium/commit/ec2c558)) +* **setOrientation:** calc view port according to automation name ([#244](https://github.com/NativeScript/nativescript-dev-appium/issues/244)) ([620a9e8](https://github.com/NativeScript/nativescript-dev-appium/commit/620a9e8)) + + ### Features * driver.getOrientation() @@ -45,7 +53,6 @@ Now: `driver.imageHelper.options.isDeviceSpecific = false;` - # [5.3.0](https://github.com/NativeScript/nativescript-dev-appium/compare/v5.2.0...v5.3.0) (2019-06-10) diff --git a/package.json b/package.json index b1ce790..8988766 100644 --- a/package.json +++ b/package.json @@ -58,4 +58,4 @@ "test": "mocha --timeout 999999", "version": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md" } -} \ No newline at end of file +} From 7246bceb7ca1bdedd23da17ab5cee7b03710a5d0 Mon Sep 17 00:00:00 2001 From: Zdravko Date: Fri, 30 Aug 2019 14:58:12 +0300 Subject: [PATCH 20/51] feat: add the ability to run adb shell commands (#251) * feature: add the ability to run adb shell commands * chore: use AndroidKeyEvent enum * Chore: add | AndroidKeyEvent to parameters of adbKeyEvent method --- lib/appium-driver.d.ts | 17 +++++++++++++++++ lib/appium-driver.ts | 29 ++++++++++++++++++++++++++++- lib/ui-element.d.ts | 10 ++++++++-- lib/ui-element.ts | 35 ++++++++++++++++++++++++++++------- lib/utils.d.ts | 1 + lib/utils.ts | 5 +++++ 6 files changed, 87 insertions(+), 10 deletions(-) diff --git a/lib/appium-driver.d.ts b/lib/appium-driver.d.ts index 3563261..3e9e72b 100644 --- a/lib/appium-driver.d.ts +++ b/lib/appium-driver.d.ts @@ -11,6 +11,7 @@ import { ImageHelper } from "./image-helper"; import { ImageOptions } from "./image-options"; import { LogType } from "./log-types"; import { DeviceOrientation } from "./enums/device-orientation"; +import { AndroidKeyEvent } from "mobile-devices-controller"; export declare class AppiumDriver { private _driver; private _wd; @@ -269,4 +270,20 @@ export declare class AppiumDriver { * This is convenient to use for some gestures on the screen */ getScreenViewPort(): IRectangle; + /** + * Android ONLY! Input key event via ADB. + * @param keyEvent The event number + */ + adbKeyEvent(keyEvent: number | AndroidKeyEvent): Promise; + /** + * Android ONLY! Send text via ADB. + * @param text The string to send + */ + adbSendText(text: string): Promise; + /** + * Android ONLY! Execute shell command via ADB. + * @param command The command name + * @param args Additional arguments + */ + adbShellCommand(command: string, args: Array): Promise; } diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index fabc4a2..2d16096 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -33,7 +33,8 @@ import { getStorage, encodeImageToBase64, ensureReportsDirExists, - checkImageLogType + checkImageLogType, + adbShellCommand } from "./utils"; import { INsCapabilities } from "./interfaces/ns-capabilities"; @@ -50,6 +51,7 @@ import { LogImageType } from "./enums/log-image-type"; import { DeviceOrientation } from "./enums/device-orientation"; import { NsCapabilities } from "./ns-capabilities"; import { AutomationName } from "./automation-name"; +import { AndroidKeyEvent } from "mobile-devices-controller"; export class AppiumDriver { private _defaultWaitTime: number = 5000; @@ -1022,4 +1024,29 @@ export class AppiumDriver { } } } + + /** + * Android ONLY! Input key event via ADB. + * @param keyEvent The event number + */ + public async adbKeyEvent(keyEvent: number | AndroidKeyEvent) { + await this.adbShellCommand("input", ["keyevent", keyEvent]); + } + + /** + * Android ONLY! Send text via ADB. + * @param text The string to send + */ + public async adbSendText(text: string) { + await this.adbShellCommand("input", ["text", text]); + } + + /** + * Android ONLY! Execute shell command via ADB. + * @param command The command name + * @param args Additional arguments + */ + public async adbShellCommand(command: string, args: Array) { + await adbShellCommand(this._driver, command, args); + } } \ No newline at end of file diff --git a/lib/ui-element.d.ts b/lib/ui-element.d.ts index 167d580..6fe5cc5 100644 --- a/lib/ui-element.d.ts +++ b/lib/ui-element.d.ts @@ -128,8 +128,9 @@ export declare class UIElement { * Send keys to field or other UI component * @param text * @param shouldClearText, default value is true + * @param useAdb, default value is false. Usable for Android ONLY ! */ - sendKeys(text: string, shouldClearText?: boolean): Promise; + sendKeys(text: string, shouldClearText?: boolean, useAdb?: boolean): Promise; /** * Type text to field or other UI component * @param text @@ -142,9 +143,14 @@ export declare class UIElement { */ pressKeycode(keyCode: number): Promise; /** - * Clears text form ui element + * Clears text from ui element */ clearText(): Promise; + /** + * Clears text from ui element with ADB. Android ONLY ! + * @param charactersCount Characters count to delete. (Optional - default value 10) + */ + adbDeleteText(charactersCount?: number): Promise; log(): Promise; refetch(): Promise; /** diff --git a/lib/ui-element.ts b/lib/ui-element.ts index 4b940e2..ae02dae 100644 --- a/lib/ui-element.ts +++ b/lib/ui-element.ts @@ -2,7 +2,7 @@ import { Point } from "./point"; import { Direction } from "./direction"; import { INsCapabilities } from "./interfaces/ns-capabilities"; import { AutomationName } from "./automation-name"; -import { calculateOffset } from "./utils"; +import { calculateOffset, adbShellCommand } from "./utils"; import { AndroidKeyEvent } from "mobile-devices-controller"; export class UIElement { @@ -382,12 +382,21 @@ export class UIElement { * Send keys to field or other UI component * @param text * @param shouldClearText, default value is true + * @param useAdb, default value is false. Usable for Android ONLY ! */ - public async sendKeys(text: string, shouldClearText: boolean = true) { - if (shouldClearText) { - await this.clearText(); + public async sendKeys(text: string, shouldClearText: boolean = true, useAdb: boolean = false) { + if (useAdb && this._args.isAndroid) { + if (shouldClearText) { + await this.adbDeleteText(); + } + await this.click(); + await adbShellCommand(this._driver, "input", ["text", text]); + } else { + if (shouldClearText) { + await this.clearText(); + } + await this._element.sendKeys(text); } - await this._element.sendKeys(text); } /** @@ -407,17 +416,29 @@ export class UIElement { * @param key code */ public async pressKeycode(keyCode: number) { - await this._driver.pressKeycode(keyCode); + await this._driver.pressKeyCode(keyCode); } /** - * Clears text form ui element + * Clears text from ui element */ public async clearText() { await this.click(); await this._element.clear(); } + /** + * Clears text from ui element with ADB. Android ONLY ! + * @param charactersCount Characters count to delete. (Optional - default value 10) + */ + public async adbDeleteText(charactersCount: number = 10) { + await this.click(); + for (let index = 0; index < charactersCount; index++) { + // Keyevent 67 Delete (backspace) + await adbShellCommand(this._driver, "input", ["keyevent", AndroidKeyEvent.KEYCODE_DEL]); + } + } + public async log() { const el = await this.element(); console.dir(el); diff --git a/lib/utils.d.ts b/lib/utils.d.ts index 764e2ca..b1be4f2 100644 --- a/lib/utils.d.ts +++ b/lib/utils.d.ts @@ -51,6 +51,7 @@ export declare function logWarn(info: any, obj?: any): void; export declare function logError(info: any, obj?: any): void; export declare function log(message: any, verbose: any): void; export declare const logColorized: (bgColor: ConsoleColor, frontColor: ConsoleColor, info: any) => void; +export declare function adbShellCommand(wd: any, command: string, args: Array): Promise; declare enum ConsoleColor { Reset = "\u001B[0m", Bright = "\u001B[1m", diff --git a/lib/utils.ts b/lib/utils.ts index 7df007e..aeda908 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -687,6 +687,11 @@ export const logColorized = (bgColor: ConsoleColor, frontColor: ConsoleColor, in console.log(`${ConsoleColor.BgYellow}${ConsoleColor.FgBlack}%s${ConsoleColor.Reset}`, info); } + +export async function adbShellCommand(wd: any, command: string, args: Array) { + await wd.execute('mobile: shell', {"command": command, "args": args}); +} + enum ConsoleColor { Reset = "\x1b[0m", Bright = "\x1b[1m", From f67c7d0688f7917c3c6f02c95d7655afaf03cfcc Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Tue, 3 Sep 2019 13:59:57 +0300 Subject: [PATCH 21/51] refactor: remove some deps that are no longer required --- lib/appium-driver.d.ts | 1 - lib/appium-driver.ts | 5 ----- package.json | 4 +--- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/appium-driver.d.ts b/lib/appium-driver.d.ts index 3e9e72b..218e72a 100644 --- a/lib/appium-driver.d.ts +++ b/lib/appium-driver.d.ts @@ -1,4 +1,3 @@ -export declare const should: any; import { ElementHelper } from "./element-helper"; import { SearchOptions } from "./search-options"; import { UIElement } from "./ui-element"; diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index 2d16096..14f25ed 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -1,10 +1,5 @@ import * as wd from "wd"; import * as webdriverio from "webdriverio"; -const chai = require("chai"); -const chaiAsPromised = require("chai-as-promised"); -chai.use(chaiAsPromised); -export const should = chai.should(); -chaiAsPromised.transferPromiseness = wd.transferPromiseness; import { ElementHelper } from "./element-helper"; import { SearchOptions } from "./search-options"; diff --git a/package.json b/package.json index 8988766..f53c94d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nativescript-dev-appium", - "version": "6.0.0", + "version": "6.1.0", "description": "A NativeScript plugin to help integrate and run Appium tests", "author": "NativeScript", "license": "MIT", @@ -33,8 +33,6 @@ "dependencies": { "app-root-path": "~2.1.0", "blink-diff": "~1.0.13", - "chai": "~4.2.0", - "chai-as-promised": "~7.1.0", "frame-comparer": "^2.0.1", "glob": "^7.1.0", "inquirer": "^6.2.0", From a10689ebd3ae6da2e126a6c18d6e77ec9b229374 Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Tue, 3 Sep 2019 14:01:16 +0300 Subject: [PATCH 22/51] fix: deep clone of cropRect from options --- lib/image-helper.d.ts | 12 ++++++------ lib/image-helper.ts | 18 ++++++++++-------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/image-helper.d.ts b/lib/image-helper.d.ts index f499009..ddc080a 100644 --- a/lib/image-helper.d.ts +++ b/lib/image-helper.d.ts @@ -17,8 +17,8 @@ export interface IImageCompareOptions { */ toleranceType?: ImageOptions; /** - * Wait miliseconds before capture creating image - * Default value is 2000 + * Wait milliseconds before capture creating image + * Default value is 5000 */ waitBeforeCreatingInitialImageCapture?: number; /** @@ -43,14 +43,14 @@ export interface IImageCompareOptions { keepOriginalImageSize?: boolean; /** * Default value is set to false. nativescript-dev-appium will recalculate view port for iOS - * so that the top/y will start from the end of status bar + * so that the top/y will start from the end of the status bar * So far appium calculates it even more and some part of safe areas are missed */ keepAppiumViewportRect?: boolean; /** - * Defines if an image is device specific or only by platform. - * Default value is true and the image will be saved in device specific directory. - * If value is set to false, image will be saved under ios or android folder. + * Defines if an image is device-specific or only by the platform. + * Default value is true and the image will be saved in device-specific directory. + * If the value is set to false, the image will be saved under ios or android folder. */ isDeviceSpecific?: boolean; /** diff --git a/lib/image-helper.ts b/lib/image-helper.ts index 7ead1ae..f546f43 100644 --- a/lib/image-helper.ts +++ b/lib/image-helper.ts @@ -29,8 +29,8 @@ export interface IImageCompareOptions { toleranceType?: ImageOptions; /** - * Wait miliseconds before capture creating image - * Default value is 2000 + * Wait milliseconds before capture creating image + * Default value is 5000 */ waitBeforeCreatingInitialImageCapture?: number; @@ -53,7 +53,7 @@ export interface IImageCompareOptions { /** * Default value is set to true which means that nativescript-dev-appium will save the image - * in original size and compare only the part which cropRectangle specifies. + * in original size and compare only the part which cropRectangle specifies. * If false, the image size will be reduced and saved by the dimensions of cropRectangle. */ keepOriginalImageSize?: boolean; @@ -61,15 +61,15 @@ export interface IImageCompareOptions { /** * Default value is set to false. nativescript-dev-appium will recalculate view port for iOS - * so that the top/y will start from the end of status bar + * so that the top/y will start from the end of the status bar * So far appium calculates it even more and some part of safe areas are missed */ keepAppiumViewportRect?: boolean; /** - * Defines if an image is device specific or only by platform. - * Default value is true and the image will be saved in device specific directory. - * If value is set to false, image will be saved under ios or android folder. + * Defines if an image is device-specific or only by the platform. + * Default value is true and the image will be saved in device-specific directory. + * If the value is set to false, the image will be saved under ios or android folder. */ isDeviceSpecific?: boolean; @@ -98,7 +98,9 @@ export class ImageHelper { }; constructor(private _args: INsCapabilities, private _driver: AppiumDriver) { - this._defaultOptions.cropRectangle = (this._args.appiumCaps && this._args.appiumCaps.viewportRect) || this._args.device.viewportRect; + if (this._args.device.viewportRect) { + ImageHelper.fullClone(this._args.device.viewportRect, this._defaultOptions.cropRectangle) + } if (!this._defaultOptions.cropRectangle || !isNumber(this._defaultOptions.cropRectangle.y)) { this._defaultOptions.cropRectangle = this._defaultOptions.cropRectangle || {}; From c5aa24eadbe75e10608c200245e4bc12e5bf38a3 Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Tue, 3 Sep 2019 14:17:46 +0300 Subject: [PATCH 23/51] fix(ui-element): getActualRectangle for Android. --- lib/parser.d.ts | 2 +- lib/ui-element.ts | 20 +++++++++---------- samples/e2e-ts/tsconfig.json | 1 + ...cript.mocha.sample.e2e-spec.mochawesome.ts | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/lib/parser.d.ts b/lib/parser.d.ts index 386ae5d..40edb2e 100644 --- a/lib/parser.d.ts +++ b/lib/parser.d.ts @@ -1,2 +1,2 @@ import { LogImageType } from "./enums/log-image-type"; -export declare const projectDir: string, projectBinary: string, pluginRoot: string, pluginBinary: string, port: number, verbose: boolean, appiumCapsLocation: string, testFolder: string, runType: string, isSauceLab: boolean, appPath: string, storage: string, testReports: string, devMode: boolean, ignoreDeviceController: boolean, wdaLocalPort: number, path: string, relaxedSecurity: boolean, cleanApp: boolean, attachToDebug: boolean, sessionId: string, startSession: boolean, capabilitiesName: string, imagesPath: string, startDeviceOptions: string, deviceTypeOrPlatform: string, device: import("mobile-devices-controller/lib/device").IDevice, driverConfig: any, logImageTypes: LogImageType[], appiumCaps: any; +export declare const projectDir: string, projectBinary: string, pluginRoot: string, pluginBinary: string, port: number, verbose: boolean, appiumCapsLocation: string, testFolder: string, runType: string, isSauceLab: boolean, appPath: string, storage: string, testReports: string, devMode: boolean, ignoreDeviceController: boolean, wdaLocalPort: number, path: string, relaxedSecurity: boolean, cleanApp: boolean, attachToDebug: boolean, sessionId: string, startSession: boolean, capabilitiesName: string, imagesPath: string, startDeviceOptions: string, deviceTypeOrPlatform: string, device: any, driverConfig: any, logImageTypes: LogImageType[], appiumCaps: any; diff --git a/lib/ui-element.ts b/lib/ui-element.ts index ae02dae..654bbca 100644 --- a/lib/ui-element.ts +++ b/lib/ui-element.ts @@ -49,7 +49,7 @@ export class UIElement { * This method is not working very good with UiAutomator2 * It is better to use click instead. */ - + public async tap() { if (this._args.automationName == AutomationName.UiAutomator2) { return await this.tapCenter(); @@ -242,16 +242,14 @@ export class UIElement { */ public async getActualRectangle() { const actRect = await this.getRectangle(); - if (this._args.isIOS) { - const density = this._args.device.config.density; - if (density) { - actRect.x *= density; - actRect.y *= density; - actRect.width *= density; - actRect.height *= density; - } else { - throw new Error("Device's density is undefined!"); - } + const density = this._args.device.config.density; + if (density) { + actRect.x *= density; + actRect.y *= density; + actRect.width *= density; + actRect.height *= density; + } else { + throw new Error("Device's density is undefined!"); } return actRect; } diff --git a/samples/e2e-ts/tsconfig.json b/samples/e2e-ts/tsconfig.json index 8f790fe..b6946e2 100644 --- a/samples/e2e-ts/tsconfig.json +++ b/samples/e2e-ts/tsconfig.json @@ -5,6 +5,7 @@ "experimentalDecorators": true, "emitDecoratorMetadata": true, "importHelpers": false, + "sourceMap": true, "types": [ ], "lib": [ "es2015", diff --git a/samples/e2e-ts/typescript.mocha.sample.e2e-spec.mochawesome.ts b/samples/e2e-ts/typescript.mocha.sample.e2e-spec.mochawesome.ts index 0d1a2ac..ccd4a08 100644 --- a/samples/e2e-ts/typescript.mocha.sample.e2e-spec.mochawesome.ts +++ b/samples/e2e-ts/typescript.mocha.sample.e2e-spec.mochawesome.ts @@ -2,7 +2,7 @@ import { AppiumDriver, createDriver, SearchOptions, nsCapabilities } from "nativ import { assert } from "chai"; const addContext = require('mochawesome/addContext'); -describe("sample scenario", () => { +describe("sample scenario", async function(){ let driver: AppiumDriver; before(async function(){ From 0d7d1017cc5c07e0847612da699629e58a5dfc67 Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Mon, 16 Sep 2019 14:17:40 +0300 Subject: [PATCH 24/51] fix: prevent crashing in case device density is missing (#256) * fix: prevent crashing in case device density is missing * update mobile-devices-controller version --- lib/appium-driver.d.ts | 4 ++-- lib/appium-driver.ts | 19 ++++++++++++------- lib/parser.d.ts | 2 +- lib/ui-element.ts | 4 ++-- package.json | 2 +- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/lib/appium-driver.d.ts b/lib/appium-driver.d.ts index 218e72a..22494f0 100644 --- a/lib/appium-driver.d.ts +++ b/lib/appium-driver.d.ts @@ -265,8 +265,8 @@ export declare class AppiumDriver { */ getScreenActualViewPort(): IRectangle; /** - * Get screen view port - * This is convenient to use for some gestures on the screen + * Get screen view port. + * Provides the view port that is needed for some gestures like swipe etc. */ getScreenViewPort(): IRectangle; /** diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index 14f25ed..57ad452 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -1001,22 +1001,27 @@ export class AppiumDriver { * Useful for image comparison */ public getScreenActualViewPort(): IRectangle { - return (this._args.appiumCaps && this._args.appiumCaps.viewportRect) || this._args.device.viewportRect; + return (this._args.device.viewportRect || this.imageHelper.options.cropRectangle); } /** - * Get screen view port - * This is convenient to use for some gestures on the screen + * Get screen view port. + * Provides the view port that is needed for some gestures like swipe etc. */ public getScreenViewPort(): IRectangle { - const rect = (this._args.appiumCaps && this._args.appiumCaps.viewportRect) || this._args.device.viewportRect; - if (rect && Object.getOwnPropertyNames(rect).length > 0) { + const rect = this.getScreenActualViewPort(); + if (rect + && Object.getOwnPropertyNames(rect).length > 0 + && this._args.appiumCaps.device.deviceScreenDensity) { return { x: rect.x / this._args.appiumCaps.device.deviceScreenDensity, y: rect.y / this._args.appiumCaps.device.deviceScreenDensity, - width: rect.x / this._args.appiumCaps.device.deviceScreenDensity, - height: rect.x / this._args.appiumCaps.device.deviceScreenDensity, + width: rect.width / this._args.appiumCaps.device.deviceScreenDensity, + height: rect.height / this._args.appiumCaps.device.deviceScreenDensity, } + } else { + logError("Device's density is undefined!"); + return rect; } } diff --git a/lib/parser.d.ts b/lib/parser.d.ts index 40edb2e..386ae5d 100644 --- a/lib/parser.d.ts +++ b/lib/parser.d.ts @@ -1,2 +1,2 @@ import { LogImageType } from "./enums/log-image-type"; -export declare const projectDir: string, projectBinary: string, pluginRoot: string, pluginBinary: string, port: number, verbose: boolean, appiumCapsLocation: string, testFolder: string, runType: string, isSauceLab: boolean, appPath: string, storage: string, testReports: string, devMode: boolean, ignoreDeviceController: boolean, wdaLocalPort: number, path: string, relaxedSecurity: boolean, cleanApp: boolean, attachToDebug: boolean, sessionId: string, startSession: boolean, capabilitiesName: string, imagesPath: string, startDeviceOptions: string, deviceTypeOrPlatform: string, device: any, driverConfig: any, logImageTypes: LogImageType[], appiumCaps: any; +export declare const projectDir: string, projectBinary: string, pluginRoot: string, pluginBinary: string, port: number, verbose: boolean, appiumCapsLocation: string, testFolder: string, runType: string, isSauceLab: boolean, appPath: string, storage: string, testReports: string, devMode: boolean, ignoreDeviceController: boolean, wdaLocalPort: number, path: string, relaxedSecurity: boolean, cleanApp: boolean, attachToDebug: boolean, sessionId: string, startSession: boolean, capabilitiesName: string, imagesPath: string, startDeviceOptions: string, deviceTypeOrPlatform: string, device: import("mobile-devices-controller/lib/device").IDevice, driverConfig: any, logImageTypes: LogImageType[], appiumCaps: any; diff --git a/lib/ui-element.ts b/lib/ui-element.ts index 654bbca..47646f5 100644 --- a/lib/ui-element.ts +++ b/lib/ui-element.ts @@ -2,7 +2,7 @@ import { Point } from "./point"; import { Direction } from "./direction"; import { INsCapabilities } from "./interfaces/ns-capabilities"; import { AutomationName } from "./automation-name"; -import { calculateOffset, adbShellCommand } from "./utils"; +import { calculateOffset, adbShellCommand, logError } from "./utils"; import { AndroidKeyEvent } from "mobile-devices-controller"; export class UIElement { @@ -249,7 +249,7 @@ export class UIElement { actRect.width *= density; actRect.height *= density; } else { - throw new Error("Device's density is undefined!"); + logError("Device's density is undefined!"); } return actRect; } diff --git a/package.json b/package.json index f53c94d..901be07 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "frame-comparer": "^2.0.1", "glob": "^7.1.0", "inquirer": "^6.2.0", - "mobile-devices-controller": "~5.0.0", + "mobile-devices-controller": "^5.0.0", "wd": "~1.11.3", "webdriverio": "~4.14.0", "yargs": "~12.0.5" From 6f91d58d47176b9221e777976ac2e6543d9939fb Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Mon, 16 Sep 2019 21:58:20 +0300 Subject: [PATCH 25/51] revert(getActualRectangle): don't cinclude density for android (#258) --- lib/ui-element.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/ui-element.ts b/lib/ui-element.ts index 47646f5..f6c09b0 100644 --- a/lib/ui-element.ts +++ b/lib/ui-element.ts @@ -243,13 +243,15 @@ export class UIElement { public async getActualRectangle() { const actRect = await this.getRectangle(); const density = this._args.device.config.density; - if (density) { - actRect.x *= density; - actRect.y *= density; - actRect.width *= density; - actRect.height *= density; - } else { - logError("Device's density is undefined!"); + if (this._args.isIOS) { + if (density) { + actRect.x *= density; + actRect.y *= density; + actRect.width *= density; + actRect.height *= density; + } else { + logError("Device's density is undefined!"); + } } return actRect; } From 9485d328b4e15aa103cf30f7a3da5a3b09a85535 Mon Sep 17 00:00:00 2001 From: Zdravko Branzov Date: Mon, 30 Sep 2019 14:11:21 +0300 Subject: [PATCH 26/51] fix: respect appium capabilities provided by the user in the json file --- lib/device-manager.ts | 4 +--- lib/image-helper.ts | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/device-manager.ts b/lib/device-manager.ts index 604e67a..dc51caa 100644 --- a/lib/device-manager.ts +++ b/lib/device-manager.ts @@ -174,9 +174,7 @@ export class DeviceManager implements IDeviceManager { apiLevel: platformVersion || args.appiumCaps.platformVersion, config: { "density": args.appiumCaps.density, "offsetPixels": args.appiumCaps.offsetPixels } } - - delete args.appiumCaps.density; - delete args.appiumCaps.offsetPixels; + DeviceManager.cleanUnsetProperties(device); return device; diff --git a/lib/image-helper.ts b/lib/image-helper.ts index f546f43..b0023be 100644 --- a/lib/image-helper.ts +++ b/lib/image-helper.ts @@ -102,9 +102,9 @@ export class ImageHelper { ImageHelper.fullClone(this._args.device.viewportRect, this._defaultOptions.cropRectangle) } if (!this._defaultOptions.cropRectangle - || !isNumber(this._defaultOptions.cropRectangle.y)) { + || !isNumber(this._defaultOptions.cropRectangle.y) || this._args.appiumCaps.offsetPixels > 0) { this._defaultOptions.cropRectangle = this._defaultOptions.cropRectangle || {}; - this._defaultOptions.cropRectangle.y = this._args.device.config.offsetPixels || 0; + this._defaultOptions.cropRectangle.y = this._args.appiumCaps.offsetPixels || this._args.device.config.offsetPixels || 0; this._defaultOptions.cropRectangle.x = 0; if (this._args.device.deviceScreenSize && this._args.device.deviceScreenSize.width && this._args.device.deviceScreenSize.height) { this._defaultOptions.cropRectangle.height = this._args.device.deviceScreenSize.height - this._defaultOptions.cropRectangle.y; From 0147399a06b29ca0abd934aaa34d9f75c405e3db Mon Sep 17 00:00:00 2001 From: Zdravko Branzov Date: Mon, 30 Sep 2019 17:39:41 +0300 Subject: [PATCH 27/51] fix: saving every image when LogImageType.everyImage is provided --- lib/image-helper.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/image-helper.ts b/lib/image-helper.ts index b0023be..3d88ad7 100644 --- a/lib/image-helper.ts +++ b/lib/image-helper.ts @@ -270,8 +270,12 @@ export class ImageHelper { const eventStartTime = Date.now().valueOf(); let counter = 1; options.timeOutSeconds *= 1000; + let pathActualImageCounter = resolvePath(this._args.reportsPath, imageName.replace(".", "_actual.")); + const shouldLogEveryImage = checkImageLogType(this._args.testReporter, LogImageType.everyImage); while ((Date.now().valueOf() - eventStartTime) <= options.timeOutSeconds && !result) { - const pathActualImageCounter = resolvePath(this._args.reportsPath, imageName.replace(".", "_actual_" + counter + ".")); + if (shouldLogEveryImage) { + pathActualImageCounter = resolvePath(this._args.reportsPath, imageName.replace(".", "_actual_" + counter + ".")); + } pathActualImage = await this._driver.saveScreenshot(pathActualImageCounter); if (!options.keepOriginalImageSize) { await this.clipRectangleImage(options.cropRectangle, pathActualImage); From efc0932ae3fdad63a5bb1b26e788589b6ce75d06 Mon Sep 17 00:00:00 2001 From: Zdravko Branzov Date: Tue, 1 Oct 2019 17:14:15 +0300 Subject: [PATCH 28/51] fix: sendKeys command to input properly string with intervals. Add parameter for chars count to be deleted --- lib/appium-driver.d.ts | 3 +++ lib/appium-driver.ts | 3 +++ lib/ui-element.d.ts | 11 +++++++---- lib/ui-element.ts | 14 +++++++++----- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/lib/appium-driver.d.ts b/lib/appium-driver.d.ts index 22494f0..5746907 100644 --- a/lib/appium-driver.d.ts +++ b/lib/appium-driver.d.ts @@ -271,16 +271,19 @@ export declare class AppiumDriver { getScreenViewPort(): IRectangle; /** * Android ONLY! Input key event via ADB. + * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too. * @param keyEvent The event number */ adbKeyEvent(keyEvent: number | AndroidKeyEvent): Promise; /** * Android ONLY! Send text via ADB. + * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too. * @param text The string to send */ adbSendText(text: string): Promise; /** * Android ONLY! Execute shell command via ADB. + * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too. * @param command The command name * @param args Additional arguments */ diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index 57ad452..513e075 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -1027,6 +1027,7 @@ export class AppiumDriver { /** * Android ONLY! Input key event via ADB. + * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too. * @param keyEvent The event number */ public async adbKeyEvent(keyEvent: number | AndroidKeyEvent) { @@ -1035,6 +1036,7 @@ export class AppiumDriver { /** * Android ONLY! Send text via ADB. + * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too. * @param text The string to send */ public async adbSendText(text: string) { @@ -1043,6 +1045,7 @@ export class AppiumDriver { /** * Android ONLY! Execute shell command via ADB. + * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too. * @param command The command name * @param args Additional arguments */ diff --git a/lib/ui-element.d.ts b/lib/ui-element.d.ts index 6fe5cc5..2bb26d3 100644 --- a/lib/ui-element.d.ts +++ b/lib/ui-element.d.ts @@ -126,11 +126,13 @@ export declare class UIElement { hold(time?: number): Promise; /** * Send keys to field or other UI component - * @param text - * @param shouldClearText, default value is true - * @param useAdb, default value is false. Usable for Android ONLY ! + * @param text The string to input + * @param shouldClearText Clears existing input before send new one - default value is 'true' + * @param useAdb default value is false. Usable for Android ONLY ! + * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too. + * @param adbDeleteCharsCount default value is 10. Usable for Android ONLY when 'useAdb' and 'shouldClearText' are True! */ - sendKeys(text: string, shouldClearText?: boolean, useAdb?: boolean): Promise; + sendKeys(text: string, shouldClearText?: boolean, useAdb?: boolean, adbDeleteCharsCount?: number): Promise; /** * Type text to field or other UI component * @param text @@ -148,6 +150,7 @@ export declare class UIElement { clearText(): Promise; /** * Clears text from ui element with ADB. Android ONLY ! + * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too. * @param charactersCount Characters count to delete. (Optional - default value 10) */ adbDeleteText(charactersCount?: number): Promise; diff --git a/lib/ui-element.ts b/lib/ui-element.ts index f6c09b0..3f89674 100644 --- a/lib/ui-element.ts +++ b/lib/ui-element.ts @@ -380,15 +380,18 @@ export class UIElement { /** * Send keys to field or other UI component - * @param text - * @param shouldClearText, default value is true - * @param useAdb, default value is false. Usable for Android ONLY ! + * @param text The string to input + * @param shouldClearText Clears existing input before send new one - default value is 'true' + * @param useAdb default value is false. Usable for Android ONLY ! + * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too. + * @param adbDeleteCharsCount default value is 10. Usable for Android ONLY when 'useAdb' and 'shouldClearText' are True! */ - public async sendKeys(text: string, shouldClearText: boolean = true, useAdb: boolean = false) { + public async sendKeys(text: string, shouldClearText: boolean = true, useAdb: boolean = false, adbDeleteCharsCount: number = 10) { if (useAdb && this._args.isAndroid) { if (shouldClearText) { - await this.adbDeleteText(); + await this.adbDeleteText(adbDeleteCharsCount); } + text = text.replace(" ","%s"); await this.click(); await adbShellCommand(this._driver, "input", ["text", text]); } else { @@ -429,6 +432,7 @@ export class UIElement { /** * Clears text from ui element with ADB. Android ONLY ! + * Must be combined with '--relaxed-security' appium flag. When not running in sauceLabs '--ignoreDeviceController' should be added too. * @param charactersCount Characters count to delete. (Optional - default value 10) */ public async adbDeleteText(charactersCount: number = 10) { From 5844f86587ab47db06a619bf9ce9e996dd497bb0 Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Thu, 3 Oct 2019 15:21:26 +0300 Subject: [PATCH 29/51] fix(ios): ios13 statusbar height (#265) * fix(ios): ios13 statusbar height --- lib/appium-driver.d.ts | 2 +- lib/appium-driver.ts | 35 +++++++++++++++++++++++++---------- lib/device-manager.d.ts | 9 +++++++-- lib/device-manager.ts | 22 +++++++++------------- lib/parser.d.ts | 2 +- package.json | 2 +- 6 files changed, 44 insertions(+), 28 deletions(-) diff --git a/lib/appium-driver.d.ts b/lib/appium-driver.d.ts index 5746907..c32d76a 100644 --- a/lib/appium-driver.d.ts +++ b/lib/appium-driver.d.ts @@ -37,7 +37,7 @@ export declare class AppiumDriver { readonly isIOS: boolean; readonly driver: any; /** - * Get the storage where test results from image comparison is logged It will be reports/app nam/device name + * Get the storage where test results from image comparison is logged. The path should be reports/app nam/device name */ readonly reportsPath: string; /** diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index 513e075..e30d196 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -11,7 +11,8 @@ import { DeviceController, IDevice, DeviceType, - AndroidController + AndroidController, + IOSController } from "mobile-devices-controller"; import { addExt, @@ -47,6 +48,7 @@ import { DeviceOrientation } from "./enums/device-orientation"; import { NsCapabilities } from "./ns-capabilities"; import { AutomationName } from "./automation-name"; import { AndroidKeyEvent } from "mobile-devices-controller"; +import { setInterval } from "timers"; export class AppiumDriver { private _defaultWaitTime: number = 5000; @@ -117,7 +119,7 @@ export class AppiumDriver { } /** - * Get the storage where test results from image comparison is logged It will be reports/app nam/device name + * Get the storage where test results from image comparison is logged. The path should be reports/app nam/device name */ get reportsPath() { return this._args.reportsPath; @@ -215,10 +217,11 @@ export class AppiumDriver { let hasStarted = false; let retries = 10; + let sessionInfoDetails; + while (retries > 0 && !hasStarted) { try { let sessionInfo; - let sessionInfoDetails; try { if (args.sessionId || args.attachToDebug) { const sessionInfos = JSON.parse(((await getSessions(args.port)) || "{}") + ''); @@ -311,7 +314,19 @@ export class AppiumDriver { await driver.updateSettings(appiumCapsFromConfig.settings); } - return new AppiumDriver(driver, wd, webio, args.driverConfig, args); + if (+sessionInfoDetails.statBarHeight === 0 + && sessionInfoDetails.platformName.toLowerCase() === "ios" + && sessionInfoDetails.platformVersion.startsWith("13")) { + try { + const devicesInfos = IOSController.devicesDisplaysInfos(); + const matches = devicesInfos.filter(d => sessionInfoDetails.deviceName.includes(d.deviceType)); + const deviceType = matches[matches.length - 1]; + args.device.viewportRect.y += deviceType.statBarHeight * deviceType.density; + } catch (error) { } + } + + const appiumDriver = new AppiumDriver(driver, wd, webio, args.driverConfig, args); + return appiumDriver; } public async updateSettings(settings: any) { @@ -1011,16 +1026,16 @@ export class AppiumDriver { public getScreenViewPort(): IRectangle { const rect = this.getScreenActualViewPort(); if (rect + && this.isIOS && Object.getOwnPropertyNames(rect).length > 0 - && this._args.appiumCaps.device.deviceScreenDensity) { + && this._args.device.deviceScreenDensity) { return { - x: rect.x / this._args.appiumCaps.device.deviceScreenDensity, - y: rect.y / this._args.appiumCaps.device.deviceScreenDensity, - width: rect.width / this._args.appiumCaps.device.deviceScreenDensity, - height: rect.height / this._args.appiumCaps.device.deviceScreenDensity, + x: rect.x / this._args.device.deviceScreenDensity, + y: rect.y / this._args.device.deviceScreenDensity, + width: rect.width / this._args.device.deviceScreenDensity, + height: rect.height / this._args.device.deviceScreenDensity, } } else { - logError("Device's density is undefined!"); return rect; } } diff --git a/lib/device-manager.d.ts b/lib/device-manager.d.ts index e535392..c6c8916 100644 --- a/lib/device-manager.d.ts +++ b/lib/device-manager.d.ts @@ -13,13 +13,18 @@ export declare class DeviceManager implements IDeviceManager { static getInstalledApps(device: IDevice): Promise; static getDefaultDevice(args: INsCapabilities, deviceName?: string, token?: string, type?: DeviceType, platformVersion?: number): IDevice; private static convertViewportRectToIRectangle; - static applyAppiumSessionInfoDetails(args: INsCapabilities, sessionInfoDetails: any): IDevice; + static applyAppiumSessionInfoDetails(args: INsCapabilities, sessionInfoDetails: any): any; static setDontKeepActivities(args: INsCapabilities, driver: any, value: any): Promise; static executeShellCommand(driver: any, commandArgs: { command: string; "args": Array; }): Promise; - static getDensity(args: INsCapabilities, driver: any): Promise; + /** + * Android only + * @param args + * @param driver + */ + static setDensity(args: INsCapabilities, driver: any): Promise; static applyDeviceAdditionsSettings(driver: any, args: INsCapabilities, sessionInfo: any): Promise; getPackageId(device: IDevice, appPath: string): string; private static cleanUnsetProperties; diff --git a/lib/device-manager.ts b/lib/device-manager.ts index dc51caa..63d3105 100644 --- a/lib/device-manager.ts +++ b/lib/device-manager.ts @@ -174,7 +174,7 @@ export class DeviceManager implements IDeviceManager { apiLevel: platformVersion || args.appiumCaps.platformVersion, config: { "density": args.appiumCaps.density, "offsetPixels": args.appiumCaps.offsetPixels } } - + DeviceManager.cleanUnsetProperties(device); return device; @@ -240,7 +240,12 @@ export class DeviceManager implements IDeviceManager { return output; } - public static async getDensity(args: INsCapabilities, driver) { + /** + * Android only + * @param args + * @param driver + */ + public static async setDensity(args: INsCapabilities, driver) { args.device.config = args.device.config || {}; if (args.appiumCaps.platformName.toLowerCase() === "android") { if (!args.ignoreDeviceController) { @@ -256,15 +261,6 @@ export class DeviceManager implements IDeviceManager { if (args.device.config.density) { args.device.config['offsetPixels'] = AndroidController.calculateScreenOffset(args.device.config.density); } - } else { - IOSController.getDevicesScreenInfo().forEach((v, k, m) => { - if (args.device.name.includes(k)) { - args.device.config = { - density: args.device.config['density'] || v.density, - offsetPixels: v.actionBarHeight - }; - } - }); } } @@ -289,8 +285,8 @@ export class DeviceManager implements IDeviceManager { args.device.config['offsetPixels'] = AndroidController.calculateScreenOffset(args.device.config.density); } - if (!density) { - await DeviceManager.getDensity(args, driver); + if (!density && !args.isIOS) { + await DeviceManager.setDensity(args, driver); density = args.device.config.density args.device.config['offsetPixels'] = AndroidController.calculateScreenOffset(args.device.config.density); } diff --git a/lib/parser.d.ts b/lib/parser.d.ts index 386ae5d..40edb2e 100644 --- a/lib/parser.d.ts +++ b/lib/parser.d.ts @@ -1,2 +1,2 @@ import { LogImageType } from "./enums/log-image-type"; -export declare const projectDir: string, projectBinary: string, pluginRoot: string, pluginBinary: string, port: number, verbose: boolean, appiumCapsLocation: string, testFolder: string, runType: string, isSauceLab: boolean, appPath: string, storage: string, testReports: string, devMode: boolean, ignoreDeviceController: boolean, wdaLocalPort: number, path: string, relaxedSecurity: boolean, cleanApp: boolean, attachToDebug: boolean, sessionId: string, startSession: boolean, capabilitiesName: string, imagesPath: string, startDeviceOptions: string, deviceTypeOrPlatform: string, device: import("mobile-devices-controller/lib/device").IDevice, driverConfig: any, logImageTypes: LogImageType[], appiumCaps: any; +export declare const projectDir: string, projectBinary: string, pluginRoot: string, pluginBinary: string, port: number, verbose: boolean, appiumCapsLocation: string, testFolder: string, runType: string, isSauceLab: boolean, appPath: string, storage: string, testReports: string, devMode: boolean, ignoreDeviceController: boolean, wdaLocalPort: number, path: string, relaxedSecurity: boolean, cleanApp: boolean, attachToDebug: boolean, sessionId: string, startSession: boolean, capabilitiesName: string, imagesPath: string, startDeviceOptions: string, deviceTypeOrPlatform: string, device: any, driverConfig: any, logImageTypes: LogImageType[], appiumCaps: any; diff --git a/package.json b/package.json index 901be07..9535a46 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "frame-comparer": "^2.0.1", "glob": "^7.1.0", "inquirer": "^6.2.0", - "mobile-devices-controller": "^5.0.0", + "mobile-devices-controller": "^5.2.0", "wd": "~1.11.3", "webdriverio": "~4.14.0", "yargs": "~12.0.5" From 3eb2e704640e6026a185e272d146d82809bc68dc Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Thu, 3 Oct 2019 17:06:57 +0300 Subject: [PATCH 30/51] docs: cut the 6.1.0 release (#266) * release: cut the 6.1.0 release * doc: release cut the 6.1.0 release --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f60915b..01c8c1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ + +# [6.1.0](https://github.com/NativeScript/nativescript-dev-appium/compare/6.0.0...6.1.0) (2019-10-03) + + +### Bug Fixes + +* deep clone of cropRect from options ([a10689e](https://github.com/NativeScript/nativescript-dev-appium/commit/a10689e)) +* prevent crashing in case device density is missing ([#256](https://github.com/NativeScript/nativescript-dev-appium/issues/256)) ([0d7d101](https://github.com/NativeScript/nativescript-dev-appium/commit/0d7d101)) +* respect appium capabilities provided by the user in the json file ([9485d32](https://github.com/NativeScript/nativescript-dev-appium/commit/9485d32)) +* saving every image when LogImageType.everyImage is provided ([0147399](https://github.com/NativeScript/nativescript-dev-appium/commit/0147399)) +* sendKeys command to input properly string with intervals. Add parameter for chars count to be deleted ([efc0932](https://github.com/NativeScript/nativescript-dev-appium/commit/efc0932)) +* **ios:** ios13 statusbar height ([#265](https://github.com/NativeScript/nativescript-dev-appium/issues/265)) ([5844f86](https://github.com/NativeScript/nativescript-dev-appium/commit/5844f86)) + + +### Features + +* add the ability to run adb shell commands ([#251](https://github.com/NativeScript/nativescript-dev-appium/issues/251)) ([7246bce](https://github.com/NativeScript/nativescript-dev-appium/commit/7246bce)) + + # [6.0.0](https://github.com/NativeScript/nativescript-dev-appium/compare/5.3.0...6.0.0) (2019-08-08) From 0aaa2c2c7a84ef603e41fa0c5d87d8ffe8fce955 Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Thu, 3 Oct 2019 17:49:25 +0300 Subject: [PATCH 31/51] chore: bumpp version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9535a46..639e473 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nativescript-dev-appium", - "version": "6.1.0", + "version": "6.2.0", "description": "A NativeScript plugin to help integrate and run Appium tests", "author": "NativeScript", "license": "MIT", From 815eca2b38f22a8191af2822b82dff3fd0e3f1ed Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Fri, 4 Oct 2019 18:34:15 +0300 Subject: [PATCH 32/51] fix: typo --- lib/appium-driver.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index e30d196..309b583 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -320,8 +320,10 @@ export class AppiumDriver { try { const devicesInfos = IOSController.devicesDisplaysInfos(); const matches = devicesInfos.filter(d => sessionInfoDetails.deviceName.includes(d.deviceType)); - const deviceType = matches[matches.length - 1]; - args.device.viewportRect.y += deviceType.statBarHeight * deviceType.density; + if (matches && matches.length > 0) { + const deviceType = matches[matches.length - 1]; + args.device.viewportRect.y += deviceType.actionBarHeight * deviceType.density; + } } catch (error) { } } From f66cffd11701372416c9681bd5d97069fb148270 Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Thu, 26 Sep 2019 17:31:21 +0300 Subject: [PATCH 33/51] feat: implement pinch, pan, rotate and scroll Still in experimental mode --- lib/appium-driver.ts | 4 +- lib/ui-element.d.ts | 66 ++++++++-- lib/ui-element.ts | 295 +++++++++++++++++++++++++++++-------------- lib/utils.d.ts | 7 +- lib/utils.ts | 79 ++++++------ 5 files changed, 309 insertions(+), 142 deletions(-) diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index 309b583..1d5a678 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -484,7 +484,7 @@ export class AppiumDriver { * @param xOffset */ public async scroll(direction: Direction, y: number, x: number, yOffset: number, xOffset: number = 0) { - await scroll(this._wd, this._driver, direction, this._webio.isIOS, y, x, yOffset, xOffset, this._args.verbose); + await scroll(this._wd, this._driver, direction, this._webio.isIOS, y, x, yOffset, xOffset); } /** @@ -504,7 +504,7 @@ export class AppiumDriver { el = await element(); isDisplayed = await el.isDisplayed(); if (!isDisplayed) { - await scroll(this._wd, this._driver, direction, this._webio.isIOS, startPoint.y, startPoint.x, offsetPoint.x, offsetPoint.y, this._args.verbose); + await scroll(this._wd, this._driver, direction, this._webio.isIOS, startPoint.y, startPoint.x, offsetPoint.x, offsetPoint.y); el = null; } } catch (error) { diff --git a/lib/ui-element.d.ts b/lib/ui-element.d.ts index 2bb26d3..704f9e7 100644 --- a/lib/ui-element.d.ts +++ b/lib/ui-element.d.ts @@ -16,6 +16,10 @@ export declare class UIElement { * Click on element */ click(): Promise; + getCenter(): Promise<{ + x: number; + y: number; + }>; tapCenter(): Promise; tapAtTheEnd(): Promise; /** @@ -26,9 +30,11 @@ export declare class UIElement { */ tap(): Promise; /** + * @experimental * Double tap on element */ doubleTap(): Promise; + longPress(duration: number): Promise; /** * Get location of element */ @@ -36,7 +42,10 @@ export declare class UIElement { /** * Get size of element */ - size(): Promise; + size(): Promise<{ + width: number; + height: number; + }>; /** * Get text of element */ @@ -113,13 +122,6 @@ export declare class UIElement { */ scrollTo(direction: Direction, elementToSearch: () => Promise, yOffset?: number, xOffset?: number, retries?: number): Promise; /** - * Drag element with specific offset - * @param direction - * @param yOffset - * @param xOffset - default value 0 - */ - drag(direction: Direction, yOffset: number, xOffset?: number): Promise; - /** * Click and hold over an element * @param time in milliseconds to increase the default press period. */ @@ -164,5 +166,51 @@ export declare class UIElement { * Swipe element left/right * @param direction */ - swipe(direction: Direction): Promise; + swipe(direction: "up" | "down" | "left" | "right"): Promise; + /** + * Drag element with specific offset + * @experimental + * @param direction + * @param yOffset + * @param xOffset - default value 0 + */ + drag(direction: Direction, yOffset: number, xOffset?: number): Promise; + /** + *@experimental + * Pan element with specific offset + * @param points where the finger should move to. + * @param initPointOffset element.getRectangle() is used as start point. In case some additional offset should be provided use this param. + */ + pan(points: { + x: number; + y: number; + }[], initPointOffset?: { + x: number; + y: number; + }): Promise; + /** + * @experimental + * This method will try to move two fingers from opposite corners. + * One finger starts from + * { x: elementRect.x + offset.x, y: elementRect.y + offset.y } + * and ends to + * { x: elementRect.x + elementRect.width - offset.x, y: elementRect.height + elementRect.y - offset.y } + * and the other finger starts from + * { x: elementRect.width + elementRect.x - offset.x, y: elementRect.height + elementRect.y - offset.y } + * and ends to + * { x: elementRect.x + offset.x, y: elementRect.y + offset.y } + */ + rotate(offset?: { + x: number; + y: number; + }): Promise; + /** + * @experimental + * @param zoomFactory - zoomIn or zoomOut. Only zoomIn action is implemented + * @param offset + */ + pinch(zoomType: "in" | "out", offset?: { + x: number; + y: number; + }): Promise; } diff --git a/lib/ui-element.ts b/lib/ui-element.ts index 3f89674..ae92cda 100644 --- a/lib/ui-element.ts +++ b/lib/ui-element.ts @@ -2,7 +2,7 @@ import { Point } from "./point"; import { Direction } from "./direction"; import { INsCapabilities } from "./interfaces/ns-capabilities"; import { AutomationName } from "./automation-name"; -import { calculateOffset, adbShellCommand, logError } from "./utils"; +import { calculateOffset, adbShellCommand, logError, wait, logInfo } from "./utils"; import { AndroidKeyEvent } from "mobile-devices-controller"; export class UIElement { @@ -24,12 +24,17 @@ export class UIElement { return await (await this.element()).click(); } + public async getCenter() { + const rect = await this.getRectangle(); + return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }; + } + public async tapCenter() { let action = new this._wd.TouchAction(this._driver); - const rect = await this.getActualRectangle(); - this._args.testReporterLog(`Tap on center element ${{ x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }}`); + const centerRect = await this.getCenter(); + this._args.testReporterLog(`Tap on center element x: ${centerRect.x} y: ${centerRect.y}`); action - .tap({ x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }); + .tap(centerRect); await action.perform(); await this._driver.sleep(150); } @@ -59,10 +64,32 @@ export class UIElement { } /** + * @experimental * Double tap on element */ public async doubleTap() { - return await this._driver.execute('mobile: doubleTap', { element: (await this.element()).value.ELEMENT }); + if (this._args.isAndroid) { + // hack double tap for android + let action = new this._wd.TouchAction(this._driver); + const rect = await this.getRectangle(); + action.press({ x: rect.x, y: rect.y }).release().perform(); + action.press({ x: rect.x, y: rect.y }).release().perform(); + await action.perform(); + } else { + // this works only for ios, otherwise it throws error + return await this._driver.execute('mobile: doubleTap', { element: this._element.value }); + } + } + + public async longPress(duration: number) { + const rect = await this.getCenter(); + console.log("LongPress at ", rect); + const action = new this._wd.TouchAction(this._driver); + action + .press({ x: rect.x, y: rect.y }) + .wait(duration) + .release(); + await action.perform(); } /** @@ -77,10 +104,9 @@ export class UIElement { /** * Get size of element */ - public async size() { + public async size(): Promise<{ width: number, height: number }> { const size = await (await this.element()).getSize(); - const point = new Point(size.height, size.width); - return point; + return size; } /** @@ -233,7 +259,7 @@ export class UIElement { public async getRectangle() { const location = await this.location(); const size = await this.size(); - const rect = { x: location.x, y: location.y, width: size.y, height: size.x }; + const rect = { x: location.x, y: location.y, width: size.width, height: size.height }; return rect; } @@ -263,40 +289,28 @@ export class UIElement { * @param xOffset */ public async scroll(direction: Direction, yOffset: number = 0, xOffset: number = 0) { - //await this._driver.execute("mobile: scroll", [{direction: 'up'}]) - //await this._driver.execute('mobile: scroll', { direction: direction === 0 ? "down" : "up", element: this._element.ELEMENT }); const location = await this.location(); const size = await this.size(); - const x = location.x === 0 ? 10 : location.x; - let y = (location.y + 15); - if (yOffset === 0) { - yOffset = location.y + size.y - 15; - } - - if (direction === Direction.down) { - y = (location.y + size.y) - 15; - if (!this._webio.isIOS) { - if (yOffset === 0) { - yOffset = location.y + size.y - 15; - } - } - } - if (direction === Direction.up) { + if (direction === Direction.down || direction === Direction.up) { if (yOffset === 0) { - yOffset = size.y - 15; + yOffset = location.y + size.height - 5; } } - const endPoint = calculateOffset(direction, y, yOffset, x, xOffset, this._webio.isIOS, false); - if (direction === Direction.down) { - //endPoint.point.y += location.y; + if (direction === Direction.left || direction === Direction.right) { + if (xOffset === 0) { + xOffset = location.x + size.width - 5; + } } - let action = new this._wd.TouchAction(this._driver); + + const endPoint = calculateOffset(direction, location.y, yOffset, location.x, xOffset, this._args.isIOS); + + const action = new this._wd.TouchAction(this._driver); action - .press({ x: x, y: y }) + .press({ x: endPoint.startPoint.x, y: endPoint.startPoint.y }) .wait(endPoint.duration) - .moveTo({ x: endPoint.point.x, y: endPoint.point.y }) + .moveTo({ x: endPoint.endPoint.x, y: endPoint.endPoint.y }) .release(); await action.perform(); await this._driver.sleep(150); @@ -329,41 +343,6 @@ export class UIElement { return el; } - /** - * Drag element with specific offset - * @param direction - * @param yOffset - * @param xOffset - default value 0 - */ - public async drag(direction: Direction, yOffset: number, xOffset: number = 0) { - const location = await this.location(); - - const x = location.x === 0 ? 10 : location.x; - const y = location.y === 0 ? 10 : location.y; - - const endPoint = calculateOffset(direction, y, yOffset, x, xOffset, this._webio.isIOS, false); - - if (this._args.isAndroid) { - let action = new this._wd.TouchAction(this._driver); - action - .longPress({ x: x, y: y }) - .wait(endPoint.duration) - .moveTo({ x: yOffset, y: yOffset }) - .release(); - await action.perform(); - } else { - await this._wd.execute(`mobile: dragFromToForDuration`, { - duration: endPoint.duration, - fromX: x, - fromY: y, - toX: xOffset, - toY: yOffset - }); - } - - await this._driver.sleep(150); - } - /** * Click and hold over an element * @param time in milliseconds to increase the default press period. @@ -473,36 +452,168 @@ export class UIElement { * Swipe element left/right * @param direction */ - public async swipe(direction: Direction) { - const rectangle = await this.getRectangle(); - const centerX = rectangle.x + rectangle.width / 2; - const centerY = rectangle.y + rectangle.height / 2; - let swipeX; - if (direction == Direction.right) { - const windowSize = await this._driver.getWindowSize(); - swipeX = windowSize.width - 10; - } else if (direction == Direction.left) { - swipeX = 10; + public async swipe(direction: "up" | "down" | "left" | "right") { + logInfo(`Swipe direction: `, direction); + if (this._args.isIOS) { + await this._driver + .execute('mobile: scroll', { + element: this._element.value, + direction: direction + }); } else { - console.log("Provided direction must be left or right !"); + try { + let scrollDirection = Direction.up; + switch (direction) { + case "down": scrollDirection = Direction.down; + break; + case "left": scrollDirection = Direction.left; + break; + case "right": scrollDirection = Direction.right; + break; + } + await this.scroll(scrollDirection); + } catch (error) { + console.log("", error); + } } + logInfo(`End swipe`); + } + + /** + * Drag element with specific offset + * @experimental + * @param direction + * @param yOffset + * @param xOffset - default value 0 + */ + public async drag(direction: Direction, yOffset: number, xOffset: number = 0) { + const location = await this.location(); + + const x = location.x === 0 ? 10 : location.x; + const y = location.y === 0 ? 10 : location.y; + + const endPoint = calculateOffset(direction, y, yOffset, x, xOffset, this._args.isIOS); if (this._args.isAndroid) { - const action = new this._wd.TouchAction(this._driver); - action.press({ x: centerX, y: centerY }) - .wait(200) - .moveTo({ x: swipeX, y: centerY }) + let action = new this._wd.TouchAction(this._driver); + action + .longPress({ x: x, y: y }) + .wait(endPoint.duration) + .moveTo({ x: yOffset, y: yOffset }) .release(); await action.perform(); - } - else { - await this._driver.execute('mobile: dragFromToForDuration', { - duration: 2.0, - fromX: centerX, - fromY: centerY, - toX: swipeX, - toY: centerY + } else { + await this._driver.execute(`mobile: dragFromToForDuration`, { + duration: endPoint.duration, + fromX: x, + fromY: y, + toX: xOffset, + toY: yOffset }); } + + await this._driver.sleep(150); } -} + + /** + *@experimental + * Pan element with specific offset + * @param points where the finger should move to. + * @param initPointOffset element.getRectangle() is used as start point. In case some additional offset should be provided use this param. + */ + public async pan(points: { x: number, y: number }[], initPointOffset: { x: number, y: number } = { x: 0, y: 0 }) { + logInfo("Start pan gesture!"); + const rect = await this.getRectangle(); + const action = new this._wd.TouchAction(this._driver); + await action.press({ x: rect.x + initPointOffset.x, y: rect.y + initPointOffset.y }).wait(100) + if (points.length > 1) { + for (let index = 1; index < points.length; index++) { + const element = points[index]; + action.moveTo({ x: element.x, y: element.y }); + } + } + + await action.release().perform(); + logInfo("End pan gesture!"); + } + + /** + * @experimental + * This method will try to move two fingers from opposite corners. + * One finger starts from + * { x: elementRect.x + offset.x, y: elementRect.y + offset.y } + * and ends to + * { x: elementRect.x + elementRect.width - offset.x, y: elementRect.height + elementRect.y - offset.y } + * and the other finger starts from + * { x: elementRect.width + elementRect.x - offset.x, y: elementRect.height + elementRect.y - offset.y } + * and ends to + * { x: elementRect.x + offset.x, y: elementRect.y + offset.y } + */ + public async rotate(offset: { x: number, y: number } = { x: 10, y: 10 }) { + logInfo("Start rotate gesture!"); + const elementRect = await this.getRectangle(); + + const startPoint = { x: elementRect.x + offset.x, y: elementRect.y + offset.y }; + const endPoint = { x: elementRect.x + elementRect.width - offset.x, y: elementRect.height + elementRect.y - offset.y }; + + const multiAction = new this._wd.MultiAction(this._driver); + const action1 = new this._wd.TouchAction(this._driver); + action1 + .press(startPoint) + .wait(100) + .moveTo(endPoint) + .wait(1000) + .release(); + multiAction.add(action1); + + const action2 = new this._wd.TouchAction(this._driver); + action2 + .press(endPoint) + .wait(100) + .moveTo({ x: startPoint.x, y: startPoint.y - 1 }) + .wait(1000) + .release(); + multiAction.add(action2); + + await multiAction.perform(); + logInfo("End rotate gesture!"); + } + + /** + * @experimental + * @param zoomFactory - zoomIn or zoomOut. Only zoomIn action is implemented + * @param offset + */ + public async pinch(zoomType: "in" | "out", offset?: { x: number, y: number }) { + logInfo("Start pinch gesture!"); + const elementRect = await this.getRectangle(); + + offset = offset || { x: elementRect.width / 2, y: elementRect.height / 2 }; + elementRect.y = elementRect.y + elementRect.height / 2; + + const endPoint = { x: offset.x, y: offset.y }; + + const startPointOne = { x: elementRect.x + 20, y: elementRect.y }; + const action1 = new this._wd.TouchAction(this._driver); + action1 + .press(startPointOne) + .wait(100) + .moveTo(endPoint) + .release() + + const multiAction = new this._wd.MultiAction(this._driver); + multiAction.add(action1); + + const startPointTwo = { x: elementRect.x + elementRect.width, y: elementRect.y }; + const action2 = new this._wd.TouchAction(this._driver); + action2 + .press(startPointTwo) + .wait(500) + .moveTo(endPoint) + .release() + multiAction.add(action2); + + await multiAction.perform(); + logInfo("End pinch gesture!"); + } +} \ No newline at end of file diff --git a/lib/utils.d.ts b/lib/utils.d.ts index b1be4f2..2cd64f7 100644 --- a/lib/utils.d.ts +++ b/lib/utils.d.ts @@ -21,8 +21,9 @@ export declare const getStorage: (args: INsCapabilities) => string; export declare function getReportPath(args: INsCapabilities): string; export declare const getRegexResultsAsArray: (regex: any, str: any) => any[]; export declare function getAppPath(caps: INsCapabilities): string; -export declare function calculateOffset(direction: any, y: number, yOffset: number, x: number, xOffset: number, isIOS: boolean, verbose: any): { - point: Point; +export declare function calculateOffset(direction: any, y: number, yOffset: number, x: number, xOffset: number, isIOS: boolean): { + startPoint: Point; + endPoint: Point; duration: number; }; /** @@ -32,7 +33,7 @@ export declare function calculateOffset(direction: any, y: number, yOffset: numb * @param yOffset * @param xOffset */ -export declare function scroll(wd: any, driver: any, direction: Direction, isIOS: boolean, y: number, x: number, yOffset: number, xOffset: number, verbose: any): Promise; +export declare function scroll(wd: any, driver: any, direction: Direction, isIOS: boolean, y: number, x: number, yOffset: number, xOffset: number): Promise; export declare const addExt: (fileName: string, ext: string) => string; export declare const isPortAvailable: (port: any) => Promise<{}>; export declare const findFreePort: (retries?: number, port?: number) => Promise; diff --git a/lib/utils.ts b/lib/utils.ts index aeda908..054130d 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -242,7 +242,7 @@ export function getStorageByDeviceName(args: INsCapabilities) { storage = createStorageFolder(storage, getDeviceName(args)); logWarn(`Images storage set to: ${storage}!`); - + return storage; } @@ -381,48 +381,55 @@ export function getAppPath(caps: INsCapabilities) { return appFullPath; } -export function calculateOffset(direction, y: number, yOffset: number, x: number, xOffset: number, isIOS: boolean, verbose) { +export function calculateOffset(direction, y: number, yOffset: number, x: number, xOffset: number, isIOS: boolean) { let speed = 10; - let yEnd = Math.abs(yOffset); - let xEnd = Math.abs(xOffset); + let yEnd = y; + let xEnd = x; let duration = Math.abs(yEnd) * speed; - if (isIOS) { - speed = 100; - if (direction === Direction.down) { - direction = -1; - yEnd = direction * yEnd; - } - if (direction === Direction.right) { - direction = -1; - xEnd = direction * xEnd; - } - } else { - if (direction === Direction.down) { - yEnd = Math.abs(yOffset - y); - } - if (direction === Direction.up) { - yEnd = direction * Math.abs((Math.abs(yOffset) + y)); - } - + if (direction === Direction.down) { + yEnd = Math.abs(y); + y = Math.abs(yOffset - y); duration = Math.abs(yOffset) * speed; + } + if (direction === Direction.up) { + yEnd = direction * Math.abs((Math.abs(yOffset) + y)); + duration = Math.abs(yOffset) * speed; + } - if (direction === Direction.right) { - xEnd = Math.abs(xOffset - x); - } + if (direction === Direction.right) { + xEnd = Math.abs(x); + x = Math.abs(xOffset - x); + duration = Math.abs(xOffset) * speed; + } - if (direction === Direction.left) { - xEnd = Math.abs(xOffset + x); + if (direction === Direction.left) { + xEnd = Math.abs(xOffset + x); + duration = Math.abs(xOffset) * speed; + const addToX = isIOS ? 50 : 5; + if (isIOS) { + x = x === 0 ? 50 : x; + } else { + x = x === 0 ? 5 : x; } - - if (yOffset < xOffset && x) { - duration = Math.abs(xOffset) * speed; + if (x === 0) { + logInfo(`Changing x to x + ${addToX}, since this will perform navigate back for ios or rise exception in android!`); } + } + if (yOffset < xOffset) { + duration = Math.abs(xOffset) * speed; } - log({ point: new Point(xEnd, yEnd), duration: duration }, verbose); + // } + logInfo("Start point point: ", new Point(x, y)); + logInfo("End point: ", new Point(xEnd, yEnd)); + logInfo("Scrolling speed point: ", duration); - return { point: new Point(xEnd, yEnd), duration: duration }; + return { + startPoint: new Point(x, y), + endPoint: new Point(xEnd, yEnd), + duration: duration + }; } /** @@ -432,19 +439,19 @@ export function calculateOffset(direction, y: number, yOffset: number, x: number * @param yOffset * @param xOffset */ -export async function scroll(wd, driver, direction: Direction, isIOS: boolean, y: number, x: number, yOffset: number, xOffset: number, verbose) { +export async function scroll(wd, driver, direction: Direction, isIOS: boolean, y: number, x: number, yOffset: number, xOffset: number) { if (x === 0) { x = 20; } if (y === 0) { y = 20; } - const endPoint = calculateOffset(direction, y, yOffset, x, xOffset, isIOS, verbose); + const endPoint = calculateOffset(direction, y, yOffset, x, xOffset, isIOS); const action = new wd.TouchAction(driver); action .press({ x: x, y: y }) .wait(endPoint.duration) - .moveTo({ x: endPoint.point.x, y: endPoint.point.y }) + .moveTo({ x: endPoint.endPoint.x, y: endPoint.endPoint.y }) .release(); await action.perform(); await driver.sleep(150); @@ -689,7 +696,7 @@ export const logColorized = (bgColor: ConsoleColor, frontColor: ConsoleColor, in export async function adbShellCommand(wd: any, command: string, args: Array) { - await wd.execute('mobile: shell', {"command": command, "args": args}); + await wd.execute('mobile: shell', { "command": command, "args": args }); } enum ConsoleColor { From f533d1201c00fbb00a99d1baf6b7128f736b217a Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Tue, 1 Oct 2019 15:06:58 +0300 Subject: [PATCH 34/51] additional logging --- lib/direction.d.ts | 2 +- lib/direction.ts | 2 +- lib/ui-element.d.ts | 4 ++-- lib/ui-element.ts | 22 ++++++++-------------- lib/utils.ts | 6 +++--- 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/lib/direction.d.ts b/lib/direction.d.ts index 8fe9856..2a7257e 100644 --- a/lib/direction.d.ts +++ b/lib/direction.d.ts @@ -1,4 +1,4 @@ -export declare const enum Direction { +export declare enum Direction { down = 0, up = 1, left = 2, diff --git a/lib/direction.ts b/lib/direction.ts index d1a3683..0fd5bd8 100644 --- a/lib/direction.ts +++ b/lib/direction.ts @@ -1,4 +1,4 @@ -export const enum Direction { +export enum Direction { down, up, left, diff --git a/lib/ui-element.d.ts b/lib/ui-element.d.ts index 704f9e7..6579214 100644 --- a/lib/ui-element.d.ts +++ b/lib/ui-element.d.ts @@ -166,7 +166,7 @@ export declare class UIElement { * Swipe element left/right * @param direction */ - swipe(direction: "up" | "down" | "left" | "right"): Promise; + swipe(direction: Direction): Promise; /** * Drag element with specific offset * @experimental @@ -174,7 +174,7 @@ export declare class UIElement { * @param yOffset * @param xOffset - default value 0 */ - drag(direction: Direction, yOffset: number, xOffset?: number): Promise; + drag(direction: Direction, yOffset: number, xOffset?: number, duration?: number): Promise; /** *@experimental * Pan element with specific offset diff --git a/lib/ui-element.ts b/lib/ui-element.ts index ae92cda..0b9d28b 100644 --- a/lib/ui-element.ts +++ b/lib/ui-element.ts @@ -452,26 +452,17 @@ export class UIElement { * Swipe element left/right * @param direction */ - public async swipe(direction: "up" | "down" | "left" | "right") { - logInfo(`Swipe direction: `, direction); + public async swipe(direction: Direction) { + logInfo(`Swipe direction: `, Direction[direction]); if (this._args.isIOS) { await this._driver .execute('mobile: scroll', { element: this._element.value, - direction: direction + direction: Direction[direction] }); } else { try { - let scrollDirection = Direction.up; - switch (direction) { - case "down": scrollDirection = Direction.down; - break; - case "left": scrollDirection = Direction.left; - break; - case "right": scrollDirection = Direction.right; - break; - } - await this.scroll(scrollDirection); + await this.scroll(direction); } catch (error) { console.log("", error); } @@ -486,13 +477,16 @@ export class UIElement { * @param yOffset * @param xOffset - default value 0 */ - public async drag(direction: Direction, yOffset: number, xOffset: number = 0) { + public async drag(direction: Direction, yOffset: number, xOffset: number = 0, duration?: number) { + direction = direction === Direction.up ? Direction.down : Direction.up; + const location = await this.location(); const x = location.x === 0 ? 10 : location.x; const y = location.y === 0 ? 10 : location.y; const endPoint = calculateOffset(direction, y, yOffset, x, xOffset, this._args.isIOS); + duration = duration || endPoint.duration; if (this._args.isAndroid) { let action = new this._wd.TouchAction(this._driver); diff --git a/lib/utils.ts b/lib/utils.ts index 054130d..421b2a7 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -420,10 +420,10 @@ export function calculateOffset(direction, y: number, yOffset: number, x: number if (yOffset < xOffset) { duration = Math.abs(xOffset) * speed; } - // } - logInfo("Start point point: ", new Point(x, y)); + + logInfo("Start point: ", new Point(x, y)); logInfo("End point: ", new Point(xEnd, yEnd)); - logInfo("Scrolling speed point: ", duration); + logInfo("Scrolling speed: ", duration); return { startPoint: new Point(x, y), From 22470a396c44eac4a78c3897f50cb37c762e11f2 Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Thu, 3 Oct 2019 17:20:39 +0300 Subject: [PATCH 35/51] chore: update mobile-devices-controller version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 639e473..f14708d 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "frame-comparer": "^2.0.1", "glob": "^7.1.0", "inquirer": "^6.2.0", - "mobile-devices-controller": "^5.2.0", + "mobile-devices-controller": "~5.2.0", "wd": "~1.11.3", "webdriverio": "~4.14.0", "yargs": "~12.0.5" From c1a993c18cd4401db9a50be25abe2b897fa3a28c Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Fri, 4 Oct 2019 23:23:44 +0300 Subject: [PATCH 36/51] fix(ios-13): remove statusbar height from viewportRect --- lib/appium-driver.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index 309b583..91f4fe7 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -322,7 +322,8 @@ export class AppiumDriver { const matches = devicesInfos.filter(d => sessionInfoDetails.deviceName.includes(d.deviceType)); if (matches && matches.length > 0) { const deviceType = matches[matches.length - 1]; - args.device.viewportRect.y += deviceType.actionBarHeight * deviceType.density; + args.device.viewportRect.y += deviceType.actionBarHeight; + args.device.viewportRect.height -= deviceType.actionBarHeight; } } catch (error) { } } From 4cfa36703d1bf7c2fd18555822abf88a81a369b5 Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Fri, 4 Oct 2019 23:44:24 +0300 Subject: [PATCH 37/51] docs: cut the 6.1.2 release --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01c8c1c..f01519e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ + +## [6.1.2](https://github.com/NativeScript/nativescript-dev-appium/compare/6.1.0...6.1.2) (2019-10-04) + + +### Bug Fixes + +* **ios-13:** remove statusbar height from viewportRect ([c1a993c](https://github.com/NativeScript/nativescript-dev-appium/commit/c1a993c)) + + + # [6.1.0](https://github.com/NativeScript/nativescript-dev-appium/compare/6.0.0...6.1.0) (2019-10-03) From ebe92e0fa431f59fec114cc055a3aa33dd4323a6 Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Mon, 7 Oct 2019 12:49:53 +0300 Subject: [PATCH 38/51] fix(ios): apply getActualRectangle (#267) --- lib/appium-driver.ts | 2 +- lib/device-manager.ts | 8 ++++---- lib/ui-element.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index 91f4fe7..6669280 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -272,7 +272,7 @@ export class AppiumDriver { logInfo("Current version of appium doesn't support appium settings!"); } - await DeviceManager.applyDeviceAdditionsSettings(driver, args, appiumCapsFromConfig); + await DeviceManager.applyDeviceAdditionsSettings(driver, args, sessionInfoDetails); hasStarted = true; } catch (error) { diff --git a/lib/device-manager.ts b/lib/device-manager.ts index 63d3105..251766d 100644 --- a/lib/device-manager.ts +++ b/lib/device-manager.ts @@ -202,7 +202,7 @@ export class DeviceManager implements IDeviceManager { args.device.config = { "density": args.device.deviceScreenDensity || args.device.config.density, "offsetPixels": +sessionInfoDetails.statBarHeight || args.device.config.offsetPixels }; } else { args.device.apiLevel = sessionInfoDetails.platformVersion; - args.device.deviceScreenDensity = sessionInfoDetails.pixelRatio; + args.device.deviceScreenDensity = sessionInfoDetails.pixelRatio || args.device.config.density; const offsetPixels = +sessionInfoDetails.viewportRect.top - +sessionInfoDetails.statBarHeight; args.device.config = { "density": sessionInfoDetails.pixelRatio || args.device.config.density, "offsetPixels": isNumber(offsetPixels) ? offsetPixels : args.device.config.offsetPixels }; } @@ -272,11 +272,11 @@ export class DeviceManager implements IDeviceManager { // } public static async applyDeviceAdditionsSettings(driver, args: INsCapabilities, sessionInfo: any) { - if ((!args.device.viewportRect || !args.device.viewportRect.x) && (!args.device.config || !args.device.config.offsetPixels)) { + if ((!args.device.viewportRect || !args.device.viewportRect.x) && (!args.device.config || !isNumber(args.device.config.offsetPixels))) { args.device.config = {}; let density: number; - if (sessionInfo && sessionInfo.length >= 1) { - density = sessionInfo[1].deviceScreenDensity ? sessionInfo[1].deviceScreenDensity / 100 : undefined; + if (sessionInfo && Object.getOwnPropertyNames(sessionInfo).length >= 1) { + density = sessionInfo.pixelRatio ? sessionInfo.pixelRatio : undefined; } if (density) { diff --git a/lib/ui-element.ts b/lib/ui-element.ts index 3f89674..0ae8c62 100644 --- a/lib/ui-element.ts +++ b/lib/ui-element.ts @@ -242,7 +242,7 @@ export class UIElement { */ public async getActualRectangle() { const actRect = await this.getRectangle(); - const density = this._args.device.config.density; + const density = this._args.device.deviceScreenDensity; if (this._args.isIOS) { if (density) { actRect.x *= density; From ac694d790d437ce6cf79e460ad4f1c39d00aaa64 Mon Sep 17 00:00:00 2001 From: Zdravko Branzov Date: Mon, 7 Oct 2019 17:08:01 +0300 Subject: [PATCH 39/51] chore: add the ability to set global tolerance and toleranceType --- lib/appium-driver.d.ts | 4 ++++ lib/appium-driver.ts | 24 +++++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/appium-driver.d.ts b/lib/appium-driver.d.ts index c32d76a..cb0645e 100644 --- a/lib/appium-driver.d.ts +++ b/lib/appium-driver.d.ts @@ -23,6 +23,8 @@ export declare class AppiumDriver { private _isAlive; private _locators; private _storageByPlatform; + private _defaultToleranceType; + private _defaultTolerance; private constructor(); readonly imageHelper: ImageHelper; defaultWaitTime: number; @@ -36,6 +38,8 @@ export declare class AppiumDriver { readonly isAndroid: boolean; readonly isIOS: boolean; readonly driver: any; + defaultToleranceType: ImageOptions; + defaultTolerance: number; /** * Get the storage where test results from image comparison is logged. The path should be reports/app nam/device name */ diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index 6669280..521cbfd 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -57,6 +57,8 @@ export class AppiumDriver { private _isAlive: boolean = false; private _locators: Locator; private _storageByPlatform: string; + private _defaultToleranceType: ImageOptions = ImageOptions.percent; + private _defaultTolerance: number = 0; private constructor(private _driver: any, private _wd, private _webio: any, private _driverConfig, private _args: INsCapabilities) { this._elementHelper = new ElementHelper(this._args); @@ -118,6 +120,22 @@ export class AppiumDriver { return this._driver; } + get defaultToleranceType(): ImageOptions { + return this._defaultToleranceType; + } + + set defaultToleranceType(toleranceType: ImageOptions) { + this._defaultToleranceType = toleranceType; + } + + get defaultTolerance(): number { + return this._defaultTolerance; + } + + set defaultTolerance(tolerance: number) { + this._defaultTolerance = tolerance; + } + /** * Get the storage where test results from image comparison is logged. The path should be reports/app nam/device name */ @@ -601,11 +619,11 @@ export class AppiumDriver { return await this.driver.getSessionId(); } - public async compareElement(element: UIElement, imageName?: string, tolerance: number = 0, timeOutSeconds: number = 3, toleranceType: ImageOptions = ImageOptions.percent) { + public async compareElement(element: UIElement, imageName?: string, tolerance: number = this._defaultTolerance, timeOutSeconds: number = 3, toleranceType: ImageOptions = this._defaultToleranceType) { return await this.compareRectangle(await element.getActualRectangle(), imageName, timeOutSeconds, tolerance, toleranceType); } - public async compareRectangle(rect: IRectangle, imageName?: string, timeOutSeconds: number = 3, tolerance: number = 0, toleranceType: ImageOptions = ImageOptions.percent) { + public async compareRectangle(rect: IRectangle, imageName?: string, timeOutSeconds: number = 3, tolerance: number = this._defaultTolerance, toleranceType: ImageOptions = this._defaultToleranceType) { imageName = imageName || this.imageHelper.testName; const options = this.imageHelper.extendOptions({ imageName: imageName, @@ -619,7 +637,7 @@ export class AppiumDriver { return await this.imageHelper.compare(options); } - public async compareScreen(imageName?: string, timeOutSeconds: number = 3, tolerance: number = 0, toleranceType: ImageOptions = ImageOptions.percent) { + public async compareScreen(imageName?: string, timeOutSeconds: number = 3, tolerance: number = this._defaultTolerance, toleranceType: ImageOptions = this._defaultToleranceType) { imageName = imageName || this.imageHelper.testName; const options = this.imageHelper.extendOptions({ imageName: imageName, From 517ee592289c3571bd5590a2b8eb713a888f78a2 Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Tue, 8 Oct 2019 17:43:20 +0300 Subject: [PATCH 40/51] fix: sctollTo --- lib/appium-driver.ts | 6 ++++-- lib/utils.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index 1b5b9cd..b61d912 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -521,13 +521,15 @@ export class AppiumDriver { while ((el === null || !isDisplayed) && retryCount > 0) { try { el = await element(); - isDisplayed = await el.isDisplayed(); + isDisplayed = el && await el.isDisplayed(); if (!isDisplayed) { - await scroll(this._wd, this._driver, direction, this._webio.isIOS, startPoint.y, startPoint.x, offsetPoint.x, offsetPoint.y); + await scroll(this._wd, this._driver, direction, this._webio.isIOS, startPoint.y, startPoint.x, offsetPoint.y, offsetPoint.x); el = null; } } catch (error) { console.log("scrollTo Error: " + error); + await scroll(this._wd, this._driver, direction, this._webio.isIOS, startPoint.y, startPoint.x, offsetPoint.y, offsetPoint.x); + el = null; } retryCount--; diff --git a/lib/utils.ts b/lib/utils.ts index 421b2a7..75985e3 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -393,7 +393,7 @@ export function calculateOffset(direction, y: number, yOffset: number, x: number duration = Math.abs(yOffset) * speed; } if (direction === Direction.up) { - yEnd = direction * Math.abs((Math.abs(yOffset) + y)); + yEnd = Math.abs((Math.abs(y - yOffset))); duration = Math.abs(yOffset) * speed; } From 8e8922fbda80f5ac781921efb35ebda5ab519052 Mon Sep 17 00:00:00 2001 From: Zdravko Branzov Date: Wed, 9 Oct 2019 18:05:06 +0300 Subject: [PATCH 41/51] fix: respect provided offset parameters when scrolling vertical and horizontal --- lib/ui-element.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/ui-element.ts b/lib/ui-element.ts index 465527c..f829165 100644 --- a/lib/ui-element.ts +++ b/lib/ui-element.ts @@ -293,12 +293,18 @@ export class UIElement { const size = await this.size(); if (direction === Direction.down || direction === Direction.up) { + if (xOffset > 0) { + location.x += xOffset; + } if (yOffset === 0) { yOffset = location.y + size.height - 5; } } if (direction === Direction.left || direction === Direction.right) { + if (yOffset > 0) { + location.y += yOffset; + } if (xOffset === 0) { xOffset = location.x + size.width - 5; } @@ -330,7 +336,7 @@ export class UIElement { while (el === null && retries >= 0) { try { el = await elementToSearch(); - if (!el || el === null || !(await el.isDisplayed())) { + if (!el || el === null || !(el && await el.isDisplayed())) { el = null; await this.scroll(direction, yOffset, xOffset); } @@ -370,7 +376,7 @@ export class UIElement { if (shouldClearText) { await this.adbDeleteText(adbDeleteCharsCount); } - text = text.replace(" ","%s"); + text = text.replace(" ", "%s"); await this.click(); await adbShellCommand(this._driver, "input", ["text", text]); } else { From 21d5d600c90bc5e21bc821e0b9273bb791dc39f8 Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Fri, 18 Oct 2019 11:30:59 +0300 Subject: [PATCH 42/51] update double tap gesture and pan --- lib/appium-driver.ts | 2 +- lib/appium-server.ts | 2 +- lib/device-manager.ts | 2 +- lib/ui-element.d.ts | 9 ++++++--- lib/ui-element.ts | 33 +++++++++++++++++++++------------ 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index b61d912..c915179 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -262,7 +262,7 @@ export class AppiumDriver { prepareApp(args); if (!args.device) { if (args.isAndroid) { - args.device = DeviceManager.getDefaultDevice(args, sessionInfo.capabilities.desired.deviceName, sessionInfo.capabilities.deviceUDID.replace("emulator-", ""), sessionInfo.capabilities.deviceUDID.includes("emulator") ? DeviceType.EMULATOR : DeviceType.SIMULATOR, sessionInfo.capabilities.desired.platformVersion || sessionInfo.capabilities.platformVersion); + args.device = DeviceManager.getDefaultDevice(args, sessionInfo.capabilities.desired.deviceName, sessionInfo.capabilities.deviceUDID.replace("emulator-", ""), sessionInfo.capabilities.deviceUDID.includes("emulator") ? DeviceType.EMULATOR : DeviceType.SIMULATOR, sessionInfo.capabilities.deviceApiLevel || sessionInfo.capabilities.platformVersion); } else { args.device = DeviceManager.getDefaultDevice(args); } diff --git a/lib/appium-server.ts b/lib/appium-server.ts index e22ef5e..85230b4 100644 --- a/lib/appium-server.ts +++ b/lib/appium-server.ts @@ -126,7 +126,7 @@ export class AppiumServer { private startAppiumServer(logLevel: string, isSauceLab: boolean) { const startingServerArgs: Array = isSauceLab ? ["--log-level", logLevel] : ["-p", this.port.toString(), "--log-level", logLevel]; - if (this._args.isAndroid && this._args.ignoreDeviceController && !this._args.isSauceLab) { + if (this._args.isAndroid) { this._args.relaxedSecurity ? startingServerArgs.push("--relaxed-security") : console.log("'relaxedSecurity' is not enabled!\nTo enabled it use '--relaxedSecurity'!"); } diff --git a/lib/device-manager.ts b/lib/device-manager.ts index 251766d..41ed8f2 100644 --- a/lib/device-manager.ts +++ b/lib/device-manager.ts @@ -171,7 +171,7 @@ export class DeviceManager implements IDeviceManager { type: type, platform: args.appiumCaps.platformName.toLowerCase(), token: token, - apiLevel: platformVersion || args.appiumCaps.platformVersion, + apiLevel: platformVersion || args.appiumCaps.deviceApiLevel || args.appiumCaps.platformVersion, config: { "density": args.appiumCaps.density, "offsetPixels": args.appiumCaps.offsetPixels } } diff --git a/lib/ui-element.d.ts b/lib/ui-element.d.ts index 6579214..ee66fbb 100644 --- a/lib/ui-element.d.ts +++ b/lib/ui-element.d.ts @@ -33,7 +33,10 @@ export declare class UIElement { * @experimental * Double tap on element */ - doubleTap(): Promise; + doubleTap(offset?: { + x: number; + y: number; + }): Promise; longPress(duration: number): Promise; /** * Get location of element @@ -178,10 +181,10 @@ export declare class UIElement { /** *@experimental * Pan element with specific offset - * @param points where the finger should move to. + * @param offsets where the finger should move to. * @param initPointOffset element.getRectangle() is used as start point. In case some additional offset should be provided use this param. */ - pan(points: { + pan(offsets: { x: number; y: number; }[], initPointOffset?: { diff --git a/lib/ui-element.ts b/lib/ui-element.ts index f829165..7e76836 100644 --- a/lib/ui-element.ts +++ b/lib/ui-element.ts @@ -67,14 +67,22 @@ export class UIElement { * @experimental * Double tap on element */ - public async doubleTap() { + public async doubleTap(offset: { x: number, y: number } = { x: 0, y: 0 }) { if (this._args.isAndroid) { // hack double tap for android - let action = new this._wd.TouchAction(this._driver); const rect = await this.getRectangle(); - action.press({ x: rect.x, y: rect.y }).release().perform(); - action.press({ x: rect.x, y: rect.y }).release().perform(); - await action.perform(); + + if (`${this._args.device.apiLevel}`.startsWith("29") + || `${this._args.device.apiLevel}`.startsWith("9.")) { + const offsetPoint = { x: (rect.x + offset.x), y: (rect.y + offset.y) }; + await adbShellCommand(this._driver, "input", ["tap", `${offsetPoint.x} ${offsetPoint.y}`]); + await adbShellCommand(this._driver, "input", ["tap", `${offsetPoint.x} ${offsetPoint.y}`]); + } else { + let action = new this._wd.TouchAction(this._driver); + action.press({ x: rect.x, y: rect.y }).release().perform(); + action.press({ x: rect.x, y: rect.y }).release().perform(); + await action.perform(); + } } else { // this works only for ios, otherwise it throws error return await this._driver.execute('mobile: doubleTap', { element: this._element.value }); @@ -518,18 +526,19 @@ export class UIElement { /** *@experimental * Pan element with specific offset - * @param points where the finger should move to. + * @param offsets where the finger should move to. * @param initPointOffset element.getRectangle() is used as start point. In case some additional offset should be provided use this param. */ - public async pan(points: { x: number, y: number }[], initPointOffset: { x: number, y: number } = { x: 0, y: 0 }) { + public async pan(offsets: { x: number, y: number }[], initPointOffset: { x: number, y: number } = { x: 0, y: 0 }) { logInfo("Start pan gesture!"); const rect = await this.getRectangle(); const action = new this._wd.TouchAction(this._driver); - await action.press({ x: rect.x + initPointOffset.x, y: rect.y + initPointOffset.y }).wait(100) - if (points.length > 1) { - for (let index = 1; index < points.length; index++) { - const element = points[index]; - action.moveTo({ x: element.x, y: element.y }); + await action.press({ x: rect.x + initPointOffset.x, y: rect.y + initPointOffset.y }); + await this.doubleTap(); + if (offsets.length > 1) { + for (let index = 1; index < offsets.length; index++) { + const p = offsets[index]; + action.moveTo({ x: rect.x + p.x, y: rect.y + p.y }); } } From 19a7cc29ff346668d791dd04631ade71a82919aa Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Mon, 21 Oct 2019 21:37:18 +0300 Subject: [PATCH 43/51] chore: update pan gesture --- lib/device-manager.ts | 1 + lib/ui-element.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/device-manager.ts b/lib/device-manager.ts index 41ed8f2..94478a5 100644 --- a/lib/device-manager.ts +++ b/lib/device-manager.ts @@ -209,6 +209,7 @@ export class DeviceManager implements IDeviceManager { args.device.statBarHeight = sessionInfoDetails.statBarHeight; args.device.viewportRect = DeviceManager.convertViewportRectToIRectangle(sessionInfoDetails.viewportRect); + args.device.token = args.device.token || sessionInfoDetails.udid; return args.device; } diff --git a/lib/ui-element.ts b/lib/ui-element.ts index 7e76836..1bcf09a 100644 --- a/lib/ui-element.ts +++ b/lib/ui-element.ts @@ -533,8 +533,7 @@ export class UIElement { logInfo("Start pan gesture!"); const rect = await this.getRectangle(); const action = new this._wd.TouchAction(this._driver); - await action.press({ x: rect.x + initPointOffset.x, y: rect.y + initPointOffset.y }); - await this.doubleTap(); + await action.press({ x: rect.x + initPointOffset.x, y: rect.y + initPointOffset.y }).wait(200); if (offsets.length > 1) { for (let index = 1; index < offsets.length; index++) { const p = offsets[index]; From b5626dd93dd15f028663777f5ba0cb6cd5f70a49 Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Wed, 30 Oct 2019 13:38:15 +0200 Subject: [PATCH 44/51] fix: apiLevel of devices --- lib/appium-driver.ts | 5 +++++ lib/device-manager.d.ts | 2 +- lib/device-manager.ts | 8 ++++---- lib/parser.d.ts | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index c915179..8967e84 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -185,6 +185,11 @@ export class AppiumDriver { } public async navBack() { + if (this.isAndroid) { + logInfo("=== Navigate back with hardware button!"); + } else { + logInfo("=== Navigate back."); + } return await this._driver.back(); } diff --git a/lib/device-manager.d.ts b/lib/device-manager.d.ts index c6c8916..17ca9a9 100644 --- a/lib/device-manager.d.ts +++ b/lib/device-manager.d.ts @@ -13,7 +13,7 @@ export declare class DeviceManager implements IDeviceManager { static getInstalledApps(device: IDevice): Promise; static getDefaultDevice(args: INsCapabilities, deviceName?: string, token?: string, type?: DeviceType, platformVersion?: number): IDevice; private static convertViewportRectToIRectangle; - static applyAppiumSessionInfoDetails(args: INsCapabilities, sessionInfoDetails: any): any; + static applyAppiumSessionInfoDetails(args: INsCapabilities, sessionInfoDetails: any): IDevice; static setDontKeepActivities(args: INsCapabilities, driver: any, value: any): Promise; static executeShellCommand(driver: any, commandArgs: { command: string; diff --git a/lib/device-manager.ts b/lib/device-manager.ts index 94478a5..385f0ab 100644 --- a/lib/device-manager.ts +++ b/lib/device-manager.ts @@ -197,7 +197,7 @@ export class DeviceManager implements IDeviceManager { const sizeArr = sessionInfoDetails.deviceScreenSize.split("x"); args.device.deviceScreenSize = { width: sizeArr[0], height: sizeArr[1] }; - args.device.apiLevel = sessionInfoDetails.deviceApiLevel; + args.device.apiLevel = sessionInfoDetails.deviceApiLevel || args.device.apiLevel; args.device.deviceScreenDensity = sessionInfoDetails.deviceScreenDensity / 100; args.device.config = { "density": args.device.deviceScreenDensity || args.device.config.density, "offsetPixels": +sessionInfoDetails.statBarHeight || args.device.config.offsetPixels }; } else { @@ -218,14 +218,14 @@ export class DeviceManager implements IDeviceManager { const status = value ? 1 : 0; try { if (args.isAndroid) { - if (!args.ignoreDeviceController) { - AndroidController.setDontKeepActivities(value, args.device); - } else if (args.relaxedSecurity) { + if (args.relaxedSecurity) { const output = await DeviceManager.executeShellCommand(driver, { command: "settings", args: ['put', 'global', 'always_finish_activities', status] }); console.log(`Output from setting always_finish_activities to ${status}: ${output}`); //check if set const check = await DeviceManager.executeShellCommand(driver, { command: "settings", args: ['get', 'global', 'always_finish_activities'] }); console.info(`Check if always_finish_activities is set correctly: ${check}`); + } else if (!args.ignoreDeviceController) { + AndroidController.setDontKeepActivities(value, args.device); } } else { // Do nothing for iOS ... diff --git a/lib/parser.d.ts b/lib/parser.d.ts index 40edb2e..386ae5d 100644 --- a/lib/parser.d.ts +++ b/lib/parser.d.ts @@ -1,2 +1,2 @@ import { LogImageType } from "./enums/log-image-type"; -export declare const projectDir: string, projectBinary: string, pluginRoot: string, pluginBinary: string, port: number, verbose: boolean, appiumCapsLocation: string, testFolder: string, runType: string, isSauceLab: boolean, appPath: string, storage: string, testReports: string, devMode: boolean, ignoreDeviceController: boolean, wdaLocalPort: number, path: string, relaxedSecurity: boolean, cleanApp: boolean, attachToDebug: boolean, sessionId: string, startSession: boolean, capabilitiesName: string, imagesPath: string, startDeviceOptions: string, deviceTypeOrPlatform: string, device: any, driverConfig: any, logImageTypes: LogImageType[], appiumCaps: any; +export declare const projectDir: string, projectBinary: string, pluginRoot: string, pluginBinary: string, port: number, verbose: boolean, appiumCapsLocation: string, testFolder: string, runType: string, isSauceLab: boolean, appPath: string, storage: string, testReports: string, devMode: boolean, ignoreDeviceController: boolean, wdaLocalPort: number, path: string, relaxedSecurity: boolean, cleanApp: boolean, attachToDebug: boolean, sessionId: string, startSession: boolean, capabilitiesName: string, imagesPath: string, startDeviceOptions: string, deviceTypeOrPlatform: string, device: import("mobile-devices-controller/lib/device").IDevice, driverConfig: any, logImageTypes: LogImageType[], appiumCaps: any; From 8130d8f023f3cca9f7f32194a932720897dc0b34 Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Fri, 1 Nov 2019 13:37:36 +0200 Subject: [PATCH 45/51] add option for derivedDataPath --- lib/appium-driver.ts | 22 +++++++++++++++------- lib/device-manager.d.ts | 2 +- lib/interfaces/ns-capabilities-args.d.ts | 1 + lib/interfaces/ns-capabilities-args.ts | 1 + lib/ns-capabilities.d.ts | 1 + lib/ns-capabilities.ts | 2 ++ lib/parser.d.ts | 2 +- lib/parser.ts | 5 +++++ 8 files changed, 27 insertions(+), 9 deletions(-) diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index 8967e84..a9f8ae4 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -30,7 +30,8 @@ import { encodeImageToBase64, ensureReportsDirExists, checkImageLogType, - adbShellCommand + adbShellCommand, + logWarn } from "./utils"; import { INsCapabilities } from "./interfaces/ns-capabilities"; @@ -280,6 +281,8 @@ export class AppiumDriver { } } catch (error) { args.verbose = true; + console.log("==============================="); + console.log("", error) if (!args.ignoreDeviceController && error && error.message && error.message.includes("Failure [INSTALL_FAILED_INSUFFICIENT_STORAGE]")) { await DeviceManager.kill(args.device); await DeviceController.startDevice(args.device); @@ -303,11 +306,11 @@ export class AppiumDriver { console.log("Retry launching appium driver!"); hasStarted = false; - if (error && error.message && error.message.includes("WebDriverAgent")) { - const freePort = await findFreePort(100, args.wdaLocalPort); - console.log("args.appiumCaps['wdaLocalPort']", freePort); - args.appiumCaps["wdaLocalPort"] = freePort; - } + // if (error && error.message && error.message.includes("WebDriverAgent")) { + // const freePort = await findFreePort(100, args.wdaLocalPort); + // console.log("args.appiumCaps['wdaLocalPort']", freePort); + // args.appiumCaps["wdaLocalPort"] = freePort; + // } } if (hasStarted) { @@ -883,7 +886,7 @@ export class AppiumDriver { } } - private static async applyAdditionalSettings(args) { + private static async applyAdditionalSettings(args: INsCapabilities) { if (args.isSauceLab) return; args.appiumCaps['udid'] = args.appiumCaps['udid'] || args.device.token; @@ -900,6 +903,11 @@ export class AppiumDriver { args.appiumCaps["wdaStartupRetries"] = 5; args.appiumCaps["shouldUseSingletonTestManager"] = args.appiumCaps.shouldUseSingletonTestManager; + if (args.derivedDataPath) { + args.appiumCaps["derivedDataPath"] = `${args.derivedDataPath}/${args.device.token}`; + logWarn('Changed derivedDataPath to: ', args.appiumCaps["derivedDataPath"]); + } + // It looks we need it for XCTest (iOS 10+ automation) if (args.appiumCaps.platformVersion >= 10 && args.wdaLocalPort) { console.log(`args.appiumCaps['wdaLocalPort']: ${args.wdaLocalPort}`); diff --git a/lib/device-manager.d.ts b/lib/device-manager.d.ts index 17ca9a9..c6c8916 100644 --- a/lib/device-manager.d.ts +++ b/lib/device-manager.d.ts @@ -13,7 +13,7 @@ export declare class DeviceManager implements IDeviceManager { static getInstalledApps(device: IDevice): Promise; static getDefaultDevice(args: INsCapabilities, deviceName?: string, token?: string, type?: DeviceType, platformVersion?: number): IDevice; private static convertViewportRectToIRectangle; - static applyAppiumSessionInfoDetails(args: INsCapabilities, sessionInfoDetails: any): IDevice; + static applyAppiumSessionInfoDetails(args: INsCapabilities, sessionInfoDetails: any): any; static setDontKeepActivities(args: INsCapabilities, driver: any, value: any): Promise; static executeShellCommand(driver: any, commandArgs: { command: string; diff --git a/lib/interfaces/ns-capabilities-args.d.ts b/lib/interfaces/ns-capabilities-args.d.ts index f1b29c9..a0c9a71 100644 --- a/lib/interfaces/ns-capabilities-args.d.ts +++ b/lib/interfaces/ns-capabilities-args.d.ts @@ -4,6 +4,7 @@ import { AutomationName } from "../automation-name"; import { ITestReporter } from "./test-reporter"; import { LogImageType } from "../enums/log-image-type"; export interface INsCapabilitiesArgs { + derivedDataPath?: string; port?: number; wdaLocalPort?: number; projectDir?: string; diff --git a/lib/interfaces/ns-capabilities-args.ts b/lib/interfaces/ns-capabilities-args.ts index 5e0ea58..2375303 100644 --- a/lib/interfaces/ns-capabilities-args.ts +++ b/lib/interfaces/ns-capabilities-args.ts @@ -5,6 +5,7 @@ import { ITestReporter } from "./test-reporter"; import { LogImageType } from "../enums/log-image-type"; export interface INsCapabilitiesArgs { + derivedDataPath?: string; port?: number; wdaLocalPort?: number; projectDir?: string; diff --git a/lib/ns-capabilities.d.ts b/lib/ns-capabilities.d.ts index ae4f03a..267ed98 100644 --- a/lib/ns-capabilities.d.ts +++ b/lib/ns-capabilities.d.ts @@ -47,6 +47,7 @@ export declare class NsCapabilities implements INsCapabilities { deviceTypeOrPlatform: string; driverConfig: any; logImageTypes: Array; + derivedDataPath: string; constructor(_parser: INsCapabilitiesArgs); readonly isAndroid: any; readonly isIOS: boolean; diff --git a/lib/ns-capabilities.ts b/lib/ns-capabilities.ts index cb99c78..b9a123a 100644 --- a/lib/ns-capabilities.ts +++ b/lib/ns-capabilities.ts @@ -53,6 +53,7 @@ export class NsCapabilities implements INsCapabilities { public deviceTypeOrPlatform: string; public driverConfig: any; public logImageTypes: Array; + public derivedDataPath: string; constructor(private _parser: INsCapabilitiesArgs) { this.projectDir = this._parser.projectDir; @@ -76,6 +77,7 @@ export class NsCapabilities implements INsCapabilities { this.isSauceLab = this._parser.isSauceLab; this.ignoreDeviceController = this._parser.ignoreDeviceController; this.wdaLocalPort = this._parser.wdaLocalPort; + this.derivedDataPath = this._parser.derivedDataPath; this.path = this._parser.path; this.capabilitiesName = this._parser.capabilitiesName; this.imagesPath = this._parser.imagesPath; diff --git a/lib/parser.d.ts b/lib/parser.d.ts index 386ae5d..b9aae80 100644 --- a/lib/parser.d.ts +++ b/lib/parser.d.ts @@ -1,2 +1,2 @@ import { LogImageType } from "./enums/log-image-type"; -export declare const projectDir: string, projectBinary: string, pluginRoot: string, pluginBinary: string, port: number, verbose: boolean, appiumCapsLocation: string, testFolder: string, runType: string, isSauceLab: boolean, appPath: string, storage: string, testReports: string, devMode: boolean, ignoreDeviceController: boolean, wdaLocalPort: number, path: string, relaxedSecurity: boolean, cleanApp: boolean, attachToDebug: boolean, sessionId: string, startSession: boolean, capabilitiesName: string, imagesPath: string, startDeviceOptions: string, deviceTypeOrPlatform: string, device: import("mobile-devices-controller/lib/device").IDevice, driverConfig: any, logImageTypes: LogImageType[], appiumCaps: any; +export declare const projectDir: string, projectBinary: string, pluginRoot: string, pluginBinary: string, port: number, verbose: boolean, appiumCapsLocation: string, testFolder: string, runType: string, isSauceLab: boolean, appPath: string, storage: string, testReports: string, devMode: boolean, ignoreDeviceController: boolean, wdaLocalPort: number, derivedDataPath: string, path: string, relaxedSecurity: boolean, cleanApp: boolean, attachToDebug: boolean, sessionId: string, startSession: boolean, capabilitiesName: string, imagesPath: string, startDeviceOptions: string, deviceTypeOrPlatform: string, device: any, driverConfig: any, logImageTypes: LogImageType[], appiumCaps: any; diff --git a/lib/parser.ts b/lib/parser.ts index 977e173..dfd21e9 100644 --- a/lib/parser.ts +++ b/lib/parser.ts @@ -64,6 +64,9 @@ const config = (() => { type: "string" }) .option("wdaLocalPort", { alias: "wda", describe: "WDA port", type: "number" }) + .options("derivedDataPath", { + describe: "set the unique derived data path root for each driver instance. This will help to avoid possible conflicts and to speed up the parallel execution", + type: "string" }) .option("verbose", { alias: "v", describe: "Log actions", type: "boolean" }) .option("path", { describe: "Execution path", default: process.cwd(), type: "string" }) .option("relaxedSecurity", { describe: "appium relaxedSecurity", default: false, type: "boolean" }) @@ -160,6 +163,7 @@ const config = (() => { pluginRoot: pluginRoot, pluginBinary: pluginBinary, wdaLocalPort: options.wdaLocalPort || process.env.npm_config_wdaLocalPort || process.env["WDA_LOCAL_PORT"] || 8410, + derivedDataPath: options.derivedDataPath || process.env.npm_config_derivedDataPath || process.env["DERIVED_DATA_PATH"], testFolder: options.testFolder || process.env.npm_config_testFolder || "e2e", runType: options.runType || process.env.npm_config_runType, appiumCapsLocation: options.appiumCapsLocation || process.env.npm_config_appiumCapsLocation || join(projectDir, options.testFolder, "config", options.capabilitiesName), @@ -208,6 +212,7 @@ export const { devMode, ignoreDeviceController, wdaLocalPort, + derivedDataPath, path, relaxedSecurity, cleanApp, From cd767984dc07b7f5f5f67a843189c8dc96ae1f51 Mon Sep 17 00:00:00 2001 From: Zdravko Branzov Date: Fri, 1 Nov 2019 17:23:01 +0200 Subject: [PATCH 46/51] fix: compareElement to use proper rectangle and generate proper diff image. --- lib/image-helper.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/image-helper.ts b/lib/image-helper.ts index 3d88ad7..52b490e 100644 --- a/lib/image-helper.ts +++ b/lib/image-helper.ts @@ -314,13 +314,13 @@ export class ImageHelper { public compareImages(options: IImageCompareOptions, actual: string, expected: string, output: string) { const clipRect = { - x: this.options.cropRectangle.x, - y: this.options.cropRectangle.y, - width: this.options.cropRectangle.width, - height: this.options.cropRectangle.height + x: options.cropRectangle.x, + y: options.cropRectangle.y, + width: options.cropRectangle.width, + height: options.cropRectangle.height } - if (!this.options.keepOriginalImageSize) { + if (!options.keepOriginalImageSize) { clipRect.x = 0; clipRect.y = 0; clipRect.width = undefined; From e98b4a23162545aeb479ec6c7a56fa0adc80ed2f Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Mon, 4 Nov 2019 15:03:10 +0200 Subject: [PATCH 47/51] fix: duration --- lib/ui-element.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ui-element.ts b/lib/ui-element.ts index 1bcf09a..62a37fa 100644 --- a/lib/ui-element.ts +++ b/lib/ui-element.ts @@ -506,13 +506,13 @@ export class UIElement { let action = new this._wd.TouchAction(this._driver); action .longPress({ x: x, y: y }) - .wait(endPoint.duration) + .wait(duration) .moveTo({ x: yOffset, y: yOffset }) .release(); await action.perform(); } else { await this._driver.execute(`mobile: dragFromToForDuration`, { - duration: endPoint.duration, + duration: duration, fromX: x, fromY: y, toX: xOffset, From 0c260b224b547b98223b279924cf10d959616c76 Mon Sep 17 00:00:00 2001 From: Zdravko Branzov Date: Thu, 7 Nov 2019 16:26:41 +0200 Subject: [PATCH 48/51] chore: move the default tolerance and toleranceType to ImageHelper --- lib/appium-driver.d.ts | 4 ---- lib/appium-driver.ts | 24 +++--------------------- lib/image-helper.d.ts | 4 ++++ lib/image-helper.ts | 18 ++++++++++++++++++ 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/lib/appium-driver.d.ts b/lib/appium-driver.d.ts index cb0645e..c32d76a 100644 --- a/lib/appium-driver.d.ts +++ b/lib/appium-driver.d.ts @@ -23,8 +23,6 @@ export declare class AppiumDriver { private _isAlive; private _locators; private _storageByPlatform; - private _defaultToleranceType; - private _defaultTolerance; private constructor(); readonly imageHelper: ImageHelper; defaultWaitTime: number; @@ -38,8 +36,6 @@ export declare class AppiumDriver { readonly isAndroid: boolean; readonly isIOS: boolean; readonly driver: any; - defaultToleranceType: ImageOptions; - defaultTolerance: number; /** * Get the storage where test results from image comparison is logged. The path should be reports/app nam/device name */ diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index 521cbfd..81c4e78 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -57,8 +57,6 @@ export class AppiumDriver { private _isAlive: boolean = false; private _locators: Locator; private _storageByPlatform: string; - private _defaultToleranceType: ImageOptions = ImageOptions.percent; - private _defaultTolerance: number = 0; private constructor(private _driver: any, private _wd, private _webio: any, private _driverConfig, private _args: INsCapabilities) { this._elementHelper = new ElementHelper(this._args); @@ -120,22 +118,6 @@ export class AppiumDriver { return this._driver; } - get defaultToleranceType(): ImageOptions { - return this._defaultToleranceType; - } - - set defaultToleranceType(toleranceType: ImageOptions) { - this._defaultToleranceType = toleranceType; - } - - get defaultTolerance(): number { - return this._defaultTolerance; - } - - set defaultTolerance(tolerance: number) { - this._defaultTolerance = tolerance; - } - /** * Get the storage where test results from image comparison is logged. The path should be reports/app nam/device name */ @@ -619,11 +601,11 @@ export class AppiumDriver { return await this.driver.getSessionId(); } - public async compareElement(element: UIElement, imageName?: string, tolerance: number = this._defaultTolerance, timeOutSeconds: number = 3, toleranceType: ImageOptions = this._defaultToleranceType) { + public async compareElement(element: UIElement, imageName?: string, tolerance: number = this.imageHelper.defaultTolerance, timeOutSeconds: number = 3, toleranceType: ImageOptions = this.imageHelper.defaultToleranceType) { return await this.compareRectangle(await element.getActualRectangle(), imageName, timeOutSeconds, tolerance, toleranceType); } - public async compareRectangle(rect: IRectangle, imageName?: string, timeOutSeconds: number = 3, tolerance: number = this._defaultTolerance, toleranceType: ImageOptions = this._defaultToleranceType) { + public async compareRectangle(rect: IRectangle, imageName?: string, timeOutSeconds: number = 3, tolerance: number = this.imageHelper.defaultTolerance, toleranceType: ImageOptions = this.imageHelper.defaultToleranceType) { imageName = imageName || this.imageHelper.testName; const options = this.imageHelper.extendOptions({ imageName: imageName, @@ -637,7 +619,7 @@ export class AppiumDriver { return await this.imageHelper.compare(options); } - public async compareScreen(imageName?: string, timeOutSeconds: number = 3, tolerance: number = this._defaultTolerance, toleranceType: ImageOptions = this._defaultToleranceType) { + public async compareScreen(imageName?: string, timeOutSeconds: number = 3, tolerance: number = this.imageHelper.defaultTolerance, toleranceType: ImageOptions = this.imageHelper.defaultToleranceType) { imageName = imageName || this.imageHelper.testName; const options = this.imageHelper.extendOptions({ imageName: imageName, diff --git a/lib/image-helper.d.ts b/lib/image-helper.d.ts index ddc080a..3842db1 100644 --- a/lib/image-helper.d.ts +++ b/lib/image-helper.d.ts @@ -64,6 +64,8 @@ export declare class ImageHelper { private _blockOutAreas; private _imagesResults; private _options; + private _defaultToleranceType; + private _defaultTolerance; private _defaultOptions; constructor(_args: INsCapabilities, _driver: AppiumDriver); static readonly pngFileExt = ".png"; @@ -80,6 +82,8 @@ export declare class ImageHelper { delta: number; options: IImageCompareOptions; blockOutAreas: IRectangle[]; + defaultToleranceType: ImageOptions; + defaultTolerance: number; compareScreen(options?: IImageCompareOptions): Promise; compareElement(element: UIElement, options?: IImageCompareOptions): Promise; compareRectangle(cropRectangle: IRectangle, options?: IImageCompareOptions): Promise; diff --git a/lib/image-helper.ts b/lib/image-helper.ts index 52b490e..139024d 100644 --- a/lib/image-helper.ts +++ b/lib/image-helper.ts @@ -83,6 +83,8 @@ export class ImageHelper { private _blockOutAreas: IRectangle[]; private _imagesResults = new Map(); private _options: IImageCompareOptions = {}; + private _defaultToleranceType: ImageOptions = ImageOptions.percent; + private _defaultTolerance: number = 0; private _defaultOptions: IImageCompareOptions = { timeOutSeconds: 2, tolerance: 0, @@ -148,6 +150,22 @@ export class ImageHelper { this._blockOutAreas = rectangles; } + get defaultToleranceType(): ImageOptions { + return this._defaultToleranceType; + } + + set defaultToleranceType(toleranceType: ImageOptions) { + this._defaultToleranceType = toleranceType; + } + + get defaultTolerance(): number { + return this._defaultTolerance; + } + + set defaultTolerance(tolerance: number) { + this._defaultTolerance = tolerance; + } + public async compareScreen(options?: IImageCompareOptions) { options = this.extendOptions(options); options.imageName = this.increaseImageName(options.imageName || this.testName, options); From baa4ff119cf4c18db22b64a5de651e3588c8ad1c Mon Sep 17 00:00:00 2001 From: SvetoslavTsenov Date: Fri, 8 Nov 2019 19:40:00 +0200 Subject: [PATCH 49/51] fix: match ios device by type and name --- lib/appium-driver.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index a9f8ae4..50fa929 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -344,10 +344,16 @@ export class AppiumDriver { && sessionInfoDetails.platformName.toLowerCase() === "ios" && sessionInfoDetails.platformVersion.startsWith("13")) { try { - const devicesInfos = IOSController.devicesDisplaysInfos(); - const matches = devicesInfos.filter(d => sessionInfoDetails.deviceName.includes(d.deviceType)); - if (matches && matches.length > 0) { - const deviceType = matches[matches.length - 1]; + const devicesInfos = IOSController.devicesDisplaysInfos() + .filter(d => sessionInfoDetails.deviceName.includes(d.deviceType)); + + if (devicesInfos.length > 0) { + // sort devices by best match - in case we have iPhone XR 13 -> it will match device type 'iPhone X' and 'iPhone XR' -> after sort we will pick first longest match + devicesInfos + .sort((a, b) => { + return sessionInfoDetails.deviceName.replace(a.deviceType, "").length - sessionInfoDetails.deviceName.replace(b.deviceType, "").length + }); + const deviceType = devicesInfos[0]; args.device.viewportRect.y += deviceType.actionBarHeight; args.device.viewportRect.height -= deviceType.actionBarHeight; } From e7d73455d451563f93ecd6e303baf9b49c9ceb1c Mon Sep 17 00:00:00 2001 From: Svetoslav Date: Mon, 11 Nov 2019 17:39:54 +0200 Subject: [PATCH 50/51] fix: resolve device type by device name (#277) --- lib/appium-driver.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index 81c4e78..dabe2b2 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -318,10 +318,16 @@ export class AppiumDriver { && sessionInfoDetails.platformName.toLowerCase() === "ios" && sessionInfoDetails.platformVersion.startsWith("13")) { try { - const devicesInfos = IOSController.devicesDisplaysInfos(); - const matches = devicesInfos.filter(d => sessionInfoDetails.deviceName.includes(d.deviceType)); - if (matches && matches.length > 0) { - const deviceType = matches[matches.length - 1]; + const devicesInfos = IOSController.devicesDisplaysInfos() + .filter(d => sessionInfoDetails.deviceName.includes(d.deviceType)); + + if (devicesInfos.length > 0) { + // sort devices by best match - in case we have iPhone XR 13 -> it will match device type 'iPhone X' and 'iPhone XR' -> after sort we will pick first longest match + devicesInfos + .sort((a, b) => { + return sessionInfoDetails.deviceName.replace(a.deviceType, "").length - sessionInfoDetails.deviceName.replace(b.deviceType, "").length + }); + const deviceType = devicesInfos[0]; args.device.viewportRect.y += deviceType.actionBarHeight; args.device.viewportRect.height -= deviceType.actionBarHeight; } From d5e0ed4655ff6f0a9686653619ccc6b261302640 Mon Sep 17 00:00:00 2001 From: Zdravko Branzov Date: Tue, 12 Nov 2019 11:52:02 +0200 Subject: [PATCH 51/51] release: cut the 6.1.3 release --- CHANGELOG.md | 11 +++++++++++ package.json | 2 +- test/device-manager.spec.ts | 6 +++--- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f01519e..b89928a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [6.1.3](https://github.com/NativeScript/nativescript-dev-appium/compare/6.1.2...6.1.3) (2019-11-12) + + +### Bug Fixes + +* **ios:** apply getActualRectangle ([#267](https://github.com/NativeScript/nativescript-dev-appium/issues/267)) ([ebe92e0](https://github.com/NativeScript/nativescript-dev-appium/commit/ebe92e0)) +* compareElement to use proper rectangle and generate proper diff image. ([cd76798](https://github.com/NativeScript/nativescript-dev-appium/commit/cd76798)) +* resolve device type by device name ([#277](https://github.com/NativeScript/nativescript-dev-appium/issues/277)) ([e7d7345](https://github.com/NativeScript/nativescript-dev-appium/commit/e7d7345)) + + + ## [6.1.2](https://github.com/NativeScript/nativescript-dev-appium/compare/6.1.0...6.1.2) (2019-10-04) diff --git a/package.json b/package.json index 639e473..7bc0aef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nativescript-dev-appium", - "version": "6.2.0", + "version": "6.1.3", "description": "A NativeScript plugin to help integrate and run Appium tests", "author": "NativeScript", "license": "MIT", diff --git a/test/device-manager.spec.ts b/test/device-manager.spec.ts index bec3568..163b998 100644 --- a/test/device-manager.spec.ts +++ b/test/device-manager.spec.ts @@ -206,8 +206,8 @@ describe("start-appium-server-ios", async () => { appPath: iosApp, appiumCaps: { platformName: Platform.IOS, - deviceName: /^iPhone XR$/, - platformVersion: /12/, + deviceName: /^iPhone XR 12$/, + platformVersion: "12.2", fullReset: true }, verbose: false @@ -275,7 +275,7 @@ describe("dev-mode-options", async () => { before("start devices", async () => { await DeviceController.startDevice({ platform: Platform.ANDROID, apiLevel: "23" }); - await DeviceController.startDevice({ platform: Platform.IOS, apiLevel: /12./, name: "iPhone XR" }); + await DeviceController.startDevice({ platform: Platform.IOS, apiLevel: "12.2", name: "iPhone XR 12" }); appiumServer = new AppiumServer({}); await appiumServer.start(8399); });