Skip to content

feat(image-comparison): implement for elements and rectangles #72

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 29, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/appium-driver.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -121,7 +122,11 @@ export declare class AppiumDriver {
swipe(y: number, x: number, yOffset: number, inertia?: number, xOffset?: number): Promise<void>;
source(): Promise<any>;
sessionId(): Promise<any>;
compareElement(element: UIElement, imageName: string): Promise<boolean>;
compareRectangles(rect: IRectangle, imageName: string, timeOutSeconds?: number, tollerance?: number): Promise<boolean>;
compareScreen(imageName: string, timeOutSeconds?: number, tollerance?: number): Promise<boolean>;
private compare(imageName, timeOutSeconds?, tollerance?, rect?);
prepareImageToCompare(filePath: string, rect: IRectangle): Promise<void>;
takeScreenshot(fileName: string): Promise<string>;
logScreenshot(fileName: string): Promise<string>;
logPageSource(fileName: string): Promise<void>;
Expand All @@ -131,4 +136,5 @@ export declare class AppiumDriver {
quit(): Promise<void>;
private convertArrayToUIElements(array, searchM, args);
private static configureLogging(driver, verbose);
private getExpectedImagePath(imageName);
}
115 changes: 75 additions & 40 deletions lib/appium-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { UIElement } from "./ui-element";
import { Direction } from "./direction";
import { Locator } from "./locators";
import {
addExt,
log,
getStorageByPlatform,
getStorageByDeviceName,
Expand All @@ -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"
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
}
11 changes: 7 additions & 4 deletions lib/image-helper.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>;
clipRectangleImage(rect: IRectangle, path: string): Promise<{}>;
readImage(path: string): Promise<any>;
}
51 changes: 39 additions & 12 deletions lib/image-helper.ts
Original file line number Diff line number Diff line change
@@ -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 {

Expand All @@ -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;
}

Expand Down Expand Up @@ -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<boolean>((resolve, reject) => {
diffOptions.run(function (error, result) {
if (error) {
Expand All @@ -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,
Expand All @@ -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,
});
Expand All @@ -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<any> {
return new Promise((resolve, reject) => {
PngJsImage.readImage(path, (err, image) => {
if (err) {
return reject(err);
}
return resolve(image);
});
})
}
}
18 changes: 17 additions & 1 deletion lib/ui-element.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export declare class UIElement {
*/
tap(): Promise<any>;
/**
* double tap
* Double tap on element
*/
doubleTap(): Promise<any>;
/**
Expand All @@ -29,6 +29,9 @@ export declare class UIElement {
* Get size of element
*/
size(): Promise<Point>;
/**
* Get text of element
*/
text(): Promise<any>;
/**
* Get web driver element
Expand All @@ -52,7 +55,20 @@ export declare class UIElement {
* @param wait
*/
waitForExist(wait?: number): Promise<any>;
/**
* Get attribute of element
* @param attr
*/
getAttribute(attr: any): Promise<any>;
/**
* Get rectangle of element
*/
getRectangle(): Promise<{
x: number;
y: number;
width: number;
height: number;
}>;
/**
* Scroll with offset from elemnt with minimum inertia
* @param direction
Expand Down
Loading