diff --git a/lib/appium-driver.d.ts b/lib/appium-driver.d.ts index 8f3d318..48d5c0c 100644 --- a/lib/appium-driver.d.ts +++ b/lib/appium-driver.d.ts @@ -5,6 +5,7 @@ import { UIElement } from "./ui-element"; import { Direction } from "./direction"; import { Locator } from "./locators"; import { INsCapabilities } from "./interfaces/ns-capabilities"; +import { IRectangle } from "./interfaces/rectangle"; import { Point } from "./point"; import { ImageHelper } from "./image-helper"; export declare class AppiumDriver { @@ -121,7 +122,11 @@ export declare class AppiumDriver { swipe(y: number, x: number, yOffset: number, inertia?: number, xOffset?: number): Promise; source(): Promise; sessionId(): Promise; + compareElement(element: UIElement, imageName: string): Promise; + compareRectangles(rect: IRectangle, imageName: string, timeOutSeconds?: number, tollerance?: number): Promise; compareScreen(imageName: string, timeOutSeconds?: number, tollerance?: number): Promise; + private compare(imageName, timeOutSeconds?, tollerance?, rect?); + prepareImageToCompare(filePath: string, rect: IRectangle): Promise; takeScreenshot(fileName: string): Promise; logScreenshot(fileName: string): Promise; logPageSource(fileName: string): Promise; @@ -131,4 +136,5 @@ export declare class AppiumDriver { quit(): Promise; private convertArrayToUIElements(array, searchM, args); private static configureLogging(driver, verbose); + private getExpectedImagePath(imageName); } diff --git a/lib/appium-driver.ts b/lib/appium-driver.ts index 26d95a9..02a46c8 100644 --- a/lib/appium-driver.ts +++ b/lib/appium-driver.ts @@ -11,6 +11,7 @@ import { UIElement } from "./ui-element"; import { Direction } from "./direction"; import { Locator } from "./locators"; import { + addExt, log, getStorageByPlatform, getStorageByDeviceName, @@ -19,9 +20,10 @@ import { getAppPath, getReportPath, calculateOffset, - scroll + scroll, } from "./utils"; import { INsCapabilities } from "./interfaces/ns-capabilities"; +import { IRectangle } from "./interfaces/rectangle"; import { Point } from "./point"; import { ImageHelper } from "./image-helper"; import { ImageOptions } from "./image-options" @@ -268,73 +270,84 @@ export class AppiumDriver { return await this.driver.getSessionId(); } + public async compareElement(element: UIElement, imageName: string, ) { + return await this.compareRectangles(await element.getRectangle(), imageName); + } + + public async compareRectangles(rect: IRectangle, imageName: string, timeOutSeconds: number = 3, tollerance: number = 0.01) { + return await this.compare(imageName, timeOutSeconds, tollerance, rect); + } + public async compareScreen(imageName: string, timeOutSeconds: number = 3, tollerance: number = 0.01) { - if (!imageName.endsWith(AppiumDriver.pngFileExt)) { - imageName = imageName.concat(AppiumDriver.pngFileExt); - } + return await this.compare(imageName, timeOutSeconds, tollerance); + } - if (!this._storageByDeviceName) { - this._storageByDeviceName = getStorageByDeviceName(this._args); - } + private async compare(imageName: string, timeOutSeconds: number = 3, tollerance: number = 0.01, rect?: IRectangle) { - let expectedImage = resolve(this._storageByDeviceName, imageName); - if (!fileExists(expectedImage)) { - if (!this._storageByPlatform) { - this._storageByPlatform = getStorageByPlatform(this._args); - } - expectedImage = resolve(this._storageByPlatform, imageName); - } - - if (!fileExists(expectedImage)) { - expectedImage = resolve(this._storageByDeviceName, imageName); - } - if (!this._logPath) { this._logPath = getReportPath(this._args); } - expectedImage = resolve(this._storageByDeviceName, imageName); + imageName = addExt(imageName, AppiumDriver.pngFileExt); - // Firts capture of screen when the expected image is not available - if (!fileExists(expectedImage)) { - await this.takeScreenshot(resolve(this._storageByDeviceName, imageName.replace(".", "_actual."))); - console.log("Remove the 'actual' suffix to continue using the image as expected one ", expectedImage); - let eventStartTime = Date.now().valueOf(); - let counter = 1; - timeOutSeconds *= 1000; + const pathExpectedImage = this.getExpectedImagePath(imageName); - while ((Date.now().valueOf() - eventStartTime) <= timeOutSeconds) { - let actualImage = await this.takeScreenshot(resolve(this._logPath, imageName.replace(".", "_actual" + "_" + counter + "."))); - counter++; + // First time capture + if (!fileExists(pathExpectedImage)) { + const pathActualImage = resolve(this._storageByDeviceName, imageName.replace(".", "_actual.")); + await this.takeScreenshot(pathActualImage); + + if (rect) { + await this._imageHelper.clipRectangleImage(rect, pathActualImage); } + console.log("Remove the 'actual' suffix to continue using the image as expected one ", pathExpectedImage); return false; } - let actualImage = await this.takeScreenshot(resolve(this._logPath, imageName.replace(".", "_actual."))); - let diffImage = actualImage.replace("actual", "diff"); - let result = await this._imageHelper.compareImages(actualImage, expectedImage, diffImage, tollerance); + // Compare + let pathActualImage = await this.takeScreenshot(resolve(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, tollerance); + + // Iterate if (!result) { - let eventStartTime = Date.now().valueOf(); + const eventStartTime = Date.now().valueOf(); let counter = 1; timeOutSeconds *= 1000; while ((Date.now().valueOf() - eventStartTime) <= timeOutSeconds && !result) { - let actualImage = await this.takeScreenshot(resolve(this._logPath, imageName.replace(".", "_actual" + "_" + counter + "."))); - result = await this._imageHelper.compareImages(actualImage, expectedImage, diffImage, tollerance); + const pathActualImageConter = resolve(this._logPath, imageName.replace(".", "_actual_" + counter + ".")); + pathActualImage = await this.takeScreenshot(pathActualImageConter); + + await this.prepareImageToCompare(pathActualImage, rect); + result = await this._imageHelper.compareImages(pathActualImage, pathExpectedImage, pathDiffImage, tollerance); counter++; } } else { - if (fileExists(diffImage)) { - unlinkSync(diffImage); + if (fileExists(pathDiffImage)) { + unlinkSync(pathDiffImage); } - if (fileExists(actualImage)) { - unlinkSync(actualImage); + if (fileExists(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); @@ -457,4 +470,26 @@ export class AppiumDriver { log(" > " + meth.magenta + path + " " + (data || "").grey, verbose); }); }; + + private getExpectedImagePath(imageName: string) { + + if (!this._storageByDeviceName) { + this._storageByDeviceName = getStorageByDeviceName(this._args); + } + + let pathExpectedImage = resolve(this._storageByDeviceName, imageName); + + if (!fileExists(pathExpectedImage)) { + if (!this._storageByPlatform) { + this._storageByPlatform = getStorageByPlatform(this._args); + } + pathExpectedImage = resolve(this._storageByPlatform, imageName); + } + + if (!fileExists(pathExpectedImage)) { + pathExpectedImage = resolve(this._storageByDeviceName, imageName); + } + + return pathExpectedImage; + } } \ No newline at end of file diff --git a/lib/image-helper.d.ts b/lib/image-helper.d.ts index c426f8b..0766810 100644 --- a/lib/image-helper.d.ts +++ b/lib/image-helper.d.ts @@ -6,18 +6,21 @@ export declare class ImageHelper { private _cropImageRect; private _blockOutAreas; constructor(_args: INsCapabilities); - readonly cropImageRect: IRectangle; - cropImageRec: IRectangle; + imageCropRect: IRectangle; blockOutAreas: IRectangle[]; imageOutputLimit(): ImageOptions; thresholdType(): ImageOptions; threshold(): number; delta(): number; - private static getOffsetPixels(args); - static cropImageDefaultParams(_args: INsCapabilities): { + static cropImageDefault(_args: INsCapabilities): { x: number; y: any; + width: any; + height: any; }; + private static getOffsetPixels(args); private runDiff(diffOptions, diffImage); compareImages(actual: string, expected: string, output: string, valueThreshold?: number, typeThreshold?: any): Promise; + clipRectangleImage(rect: IRectangle, path: string): Promise<{}>; + readImage(path: string): Promise; } diff --git a/lib/image-helper.ts b/lib/image-helper.ts index eda6f38..257e5d6 100644 --- a/lib/image-helper.ts +++ b/lib/image-helper.ts @@ -1,7 +1,9 @@ -import * as blinkDiff from "blink-diff"; +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 { Point } from "./point"; export class ImageHelper { @@ -11,11 +13,11 @@ export class ImageHelper { constructor(private _args: INsCapabilities) { } - get cropImageRect() { + get imageCropRect(): IRectangle { return this._cropImageRect; } - set cropImageRec(rect: IRectangle) { + set imageCropRect(rect: IRectangle) { this._cropImageRect = rect; } @@ -43,15 +45,15 @@ export class ImageHelper { return 20; } - private static getOffsetPixels(args: INsCapabilities) { - return args.device.config ? args.device.config.offsetPixels : 0 + public static cropImageDefault(_args: INsCapabilities) { + return { x: 0, y: ImageHelper.getOffsetPixels(_args), width: undefined, height: undefined }; } - public static cropImageDefaultParams(_args: INsCapabilities) { - return { x: 0, y: ImageHelper.getOffsetPixels(_args) }; + private static getOffsetPixels(args: INsCapabilities) { + return args.device.config ? args.device.config.offsetPixels : 0 } - private runDiff(diffOptions: blinkDiff, diffImage: string) { + private runDiff(diffOptions: BlinkDiff, diffImage: string) { return new Promise((resolve, reject) => { diffOptions.run(function (error, result) { if (error) { @@ -77,8 +79,7 @@ export class ImageHelper { } public compareImages(actual: string, expected: string, output: string, valueThreshold: number = this.threshold(), typeThreshold: any = ImageOptions.pixel) { - const rectToCrop = this._cropImageRect || ImageHelper.cropImageDefaultParams(this._args); - let diff = new blinkDiff({ + const diff = new BlinkDiff({ imageAPath: actual, imageBPath: expected, @@ -88,8 +89,8 @@ export class ImageHelper { threshold: valueThreshold, delta: this.delta(), - cropImageA: rectToCrop, - cropImageB: rectToCrop, + cropImageA: this._cropImageRect, + cropImageB: this._cropImageRect, blockOut: this._blockOutAreas, verbose: this._args.verbose, }); @@ -98,4 +99,30 @@ export class ImageHelper { this._blockOutAreas = undefined; return result; } + + public async clipRectangleImage(rect: IRectangle, path: string) { + let imageToClip: PngJsImage; + imageToClip = await this.readImage(path); + 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(); + }); + + }) + } + + public readImage(path: string): Promise { + return new Promise((resolve, reject) => { + PngJsImage.readImage(path, (err, image) => { + if (err) { + return reject(err); + } + return resolve(image); + }); + }) + } } diff --git a/lib/ui-element.d.ts b/lib/ui-element.d.ts index a9b5be3..12248d5 100644 --- a/lib/ui-element.d.ts +++ b/lib/ui-element.d.ts @@ -18,7 +18,7 @@ export declare class UIElement { */ tap(): Promise; /** - * double tap + * Double tap on element */ doubleTap(): Promise; /** @@ -29,6 +29,9 @@ export declare class UIElement { * Get size of element */ size(): Promise; + /** + * Get text of element + */ text(): Promise; /** * Get web driver element @@ -52,7 +55,20 @@ export declare class UIElement { * @param wait */ waitForExist(wait?: number): Promise; + /** + * Get attribute of element + * @param attr + */ getAttribute(attr: any): Promise; + /** + * Get rectangle of element + */ + getRectangle(): Promise<{ + x: number; + y: number; + width: number; + height: number; + }>; /** * Scroll with offset from elemnt with minimum inertia * @param direction diff --git a/lib/ui-element.ts b/lib/ui-element.ts index 1ac89ac..a7e20cd 100644 --- a/lib/ui-element.ts +++ b/lib/ui-element.ts @@ -1,10 +1,10 @@ import { Point } from "./point"; import { Direction } from "./direction"; +import { IRectangle } from "./interfaces/rectangle"; import { calculateOffset } from "./utils"; export class UIElement { - constructor(private _element: any, private _driver: any, private _wd: any, private _webio: any, private _searchMethod: string, private _searchParams: string, private _index?: number) { - } + constructor(private _element: any, private _driver: any, private _wd: any, private _webio: any, private _searchMethod: string, private _searchParams: string, private _index?: number) { } /** * Click on element @@ -21,7 +21,7 @@ export class UIElement { } /** - * double tap + * Double tap on element */ public async doubleTap() { return await this._driver.execute('mobile: doubleTap', { element: (await this.element()).value.ELEMENT }); @@ -42,10 +42,12 @@ export class UIElement { public async size() { const size = await (await this.element()).getSize(); const point = new Point(size.height, size.width); - return point; } + /** + * Get text of element + */ public async text() { return await (await this.element()).text(); } @@ -89,10 +91,24 @@ export class UIElement { return this._webio.waitForExist(this._searchParams, wait, false); } + /** + * Get attribute of element + * @param attr + */ public async getAttribute(attr) { return await (await this.element()).getAttribute(attr); } + /** + * Get rectangle of element + */ + 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 }; + return rect; + } + /** * Scroll with offset from elemnt with minimum inertia * @param direction @@ -112,7 +128,7 @@ export class UIElement { if (direction === Direction.down) { y = (location.y + size.y) - 15; - + if (!this._webio.isIOS) { if (yOffset === 0) { yOffset = location.y + size.y - 15; @@ -197,7 +213,7 @@ export class UIElement { public async hold() { let action = new this._wd.TouchAction(this._driver); action - .longPress({el: await this.element()}) + .longPress({ el: await this.element() }) .release(); await action.perform(); await this._driver.sleep(150); diff --git a/lib/utils.d.ts b/lib/utils.d.ts index 7ed8dad..d6a8f71 100644 --- a/lib/utils.d.ts +++ b/lib/utils.d.ts @@ -33,3 +33,4 @@ export declare function calculateOffset(direction: any, y: number, yOffset: numb * @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 const addExt: (fileName: string, ext: string) => string; diff --git a/lib/utils.ts b/lib/utils.ts index a70087f..9c723ea 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -386,3 +386,5 @@ function createStorageFolder(storage, direcotry) { return storage; } + +export const addExt = (fileName: string, ext: string) => { return fileName.endsWith(ext) ? fileName : fileName.concat(ext); } \ No newline at end of file