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));