Skip to content

Commit e95c6ad

Browse files
author
Vasil Chimev
authored
feat(image-comparison): implement for elements and rectangles (#72)
* feat(image-comparison): implement for elements and rectangles * feat(image-comparison): first time capture of element/rectangle * refactor: image-compaarison for elements and rectangles
1 parent 0058296 commit e95c6ad

File tree

8 files changed

+169
-63
lines changed

8 files changed

+169
-63
lines changed

lib/appium-driver.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { UIElement } from "./ui-element";
55
import { Direction } from "./direction";
66
import { Locator } from "./locators";
77
import { INsCapabilities } from "./interfaces/ns-capabilities";
8+
import { IRectangle } from "./interfaces/rectangle";
89
import { Point } from "./point";
910
import { ImageHelper } from "./image-helper";
1011
export declare class AppiumDriver {
@@ -121,7 +122,11 @@ export declare class AppiumDriver {
121122
swipe(y: number, x: number, yOffset: number, inertia?: number, xOffset?: number): Promise<void>;
122123
source(): Promise<any>;
123124
sessionId(): Promise<any>;
125+
compareElement(element: UIElement, imageName: string): Promise<boolean>;
126+
compareRectangles(rect: IRectangle, imageName: string, timeOutSeconds?: number, tollerance?: number): Promise<boolean>;
124127
compareScreen(imageName: string, timeOutSeconds?: number, tollerance?: number): Promise<boolean>;
128+
private compare(imageName, timeOutSeconds?, tollerance?, rect?);
129+
prepareImageToCompare(filePath: string, rect: IRectangle): Promise<void>;
125130
takeScreenshot(fileName: string): Promise<string>;
126131
logScreenshot(fileName: string): Promise<string>;
127132
logPageSource(fileName: string): Promise<void>;
@@ -131,4 +136,5 @@ export declare class AppiumDriver {
131136
quit(): Promise<void>;
132137
private convertArrayToUIElements(array, searchM, args);
133138
private static configureLogging(driver, verbose);
139+
private getExpectedImagePath(imageName);
134140
}

lib/appium-driver.ts

Lines changed: 75 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { UIElement } from "./ui-element";
1111
import { Direction } from "./direction";
1212
import { Locator } from "./locators";
1313
import {
14+
addExt,
1415
log,
1516
getStorageByPlatform,
1617
getStorageByDeviceName,
@@ -19,9 +20,10 @@ import {
1920
getAppPath,
2021
getReportPath,
2122
calculateOffset,
22-
scroll
23+
scroll,
2324
} from "./utils";
2425
import { INsCapabilities } from "./interfaces/ns-capabilities";
26+
import { IRectangle } from "./interfaces/rectangle";
2527
import { Point } from "./point";
2628
import { ImageHelper } from "./image-helper";
2729
import { ImageOptions } from "./image-options"
@@ -268,73 +270,84 @@ export class AppiumDriver {
268270
return await this.driver.getSessionId();
269271
}
270272

273+
public async compareElement(element: UIElement, imageName: string, ) {
274+
return await this.compareRectangles(await element.getRectangle(), imageName);
275+
}
276+
277+
public async compareRectangles(rect: IRectangle, imageName: string, timeOutSeconds: number = 3, tollerance: number = 0.01) {
278+
return await this.compare(imageName, timeOutSeconds, tollerance, rect);
279+
}
280+
271281
public async compareScreen(imageName: string, timeOutSeconds: number = 3, tollerance: number = 0.01) {
272-
if (!imageName.endsWith(AppiumDriver.pngFileExt)) {
273-
imageName = imageName.concat(AppiumDriver.pngFileExt);
274-
}
282+
return await this.compare(imageName, timeOutSeconds, tollerance);
283+
}
275284

276-
if (!this._storageByDeviceName) {
277-
this._storageByDeviceName = getStorageByDeviceName(this._args);
278-
}
285+
private async compare(imageName: string, timeOutSeconds: number = 3, tollerance: number = 0.01, rect?: IRectangle) {
279286

280-
let expectedImage = resolve(this._storageByDeviceName, imageName);
281-
if (!fileExists(expectedImage)) {
282-
if (!this._storageByPlatform) {
283-
this._storageByPlatform = getStorageByPlatform(this._args);
284-
}
285-
expectedImage = resolve(this._storageByPlatform, imageName);
286-
}
287-
288-
if (!fileExists(expectedImage)) {
289-
expectedImage = resolve(this._storageByDeviceName, imageName);
290-
}
291-
292287
if (!this._logPath) {
293288
this._logPath = getReportPath(this._args);
294289
}
295290

296-
expectedImage = resolve(this._storageByDeviceName, imageName);
291+
imageName = addExt(imageName, AppiumDriver.pngFileExt);
297292

298-
// Firts capture of screen when the expected image is not available
299-
if (!fileExists(expectedImage)) {
300-
await this.takeScreenshot(resolve(this._storageByDeviceName, imageName.replace(".", "_actual.")));
301-
console.log("Remove the 'actual' suffix to continue using the image as expected one ", expectedImage);
302-
let eventStartTime = Date.now().valueOf();
303-
let counter = 1;
304-
timeOutSeconds *= 1000;
293+
const pathExpectedImage = this.getExpectedImagePath(imageName);
305294

306-
while ((Date.now().valueOf() - eventStartTime) <= timeOutSeconds) {
307-
let actualImage = await this.takeScreenshot(resolve(this._logPath, imageName.replace(".", "_actual" + "_" + counter + ".")));
308-
counter++;
295+
// First time capture
296+
if (!fileExists(pathExpectedImage)) {
297+
const pathActualImage = resolve(this._storageByDeviceName, imageName.replace(".", "_actual."));
298+
await this.takeScreenshot(pathActualImage);
299+
300+
if (rect) {
301+
await this._imageHelper.clipRectangleImage(rect, pathActualImage);
309302
}
310303

304+
console.log("Remove the 'actual' suffix to continue using the image as expected one ", pathExpectedImage);
311305
return false;
312306
}
313307

314-
let actualImage = await this.takeScreenshot(resolve(this._logPath, imageName.replace(".", "_actual.")));
315-
let diffImage = actualImage.replace("actual", "diff");
316-
let result = await this._imageHelper.compareImages(actualImage, expectedImage, diffImage, tollerance);
308+
// Compare
309+
let pathActualImage = await this.takeScreenshot(resolve(this._logPath, imageName.replace(".", "_actual.")));
310+
const pathDiffImage = pathActualImage.replace("actual", "diff");
311+
312+
await this.prepareImageToCompare(pathActualImage, rect);
313+
let result = await this._imageHelper.compareImages(pathActualImage, pathExpectedImage, pathDiffImage, tollerance);
314+
315+
// Iterate
317316
if (!result) {
318-
let eventStartTime = Date.now().valueOf();
317+
const eventStartTime = Date.now().valueOf();
319318
let counter = 1;
320319
timeOutSeconds *= 1000;
321320
while ((Date.now().valueOf() - eventStartTime) <= timeOutSeconds && !result) {
322-
let actualImage = await this.takeScreenshot(resolve(this._logPath, imageName.replace(".", "_actual" + "_" + counter + ".")));
323-
result = await this._imageHelper.compareImages(actualImage, expectedImage, diffImage, tollerance);
321+
const pathActualImageConter = resolve(this._logPath, imageName.replace(".", "_actual_" + counter + "."));
322+
pathActualImage = await this.takeScreenshot(pathActualImageConter);
323+
324+
await this.prepareImageToCompare(pathActualImage, rect);
325+
result = await this._imageHelper.compareImages(pathActualImage, pathExpectedImage, pathDiffImage, tollerance);
324326
counter++;
325327
}
326328
} else {
327-
if (fileExists(diffImage)) {
328-
unlinkSync(diffImage);
329+
if (fileExists(pathDiffImage)) {
330+
unlinkSync(pathDiffImage);
329331
}
330-
if (fileExists(actualImage)) {
331-
unlinkSync(actualImage);
332+
if (fileExists(pathActualImage)) {
333+
unlinkSync(pathActualImage);
332334
}
333335
}
334336

337+
this._imageHelper.imageCropRect = undefined;
335338
return result;
336339
}
337340

341+
public async prepareImageToCompare(filePath: string, rect: IRectangle) {
342+
if (rect) {
343+
await this._imageHelper.clipRectangleImage(rect, filePath);
344+
const rectToCrop = { x: 0, y: 0, width: undefined, height: undefined };
345+
this._imageHelper.imageCropRect = rectToCrop;
346+
} else {
347+
this._imageHelper.imageCropRect = ImageHelper.cropImageDefault(this._args);
348+
}
349+
}
350+
338351
public takeScreenshot(fileName: string) {
339352
if (!fileName.endsWith(AppiumDriver.pngFileExt)) {
340353
fileName = fileName.concat(AppiumDriver.pngFileExt);
@@ -457,4 +470,26 @@ export class AppiumDriver {
457470
log(" > " + meth.magenta + path + " " + (data || "").grey, verbose);
458471
});
459472
};
473+
474+
private getExpectedImagePath(imageName: string) {
475+
476+
if (!this._storageByDeviceName) {
477+
this._storageByDeviceName = getStorageByDeviceName(this._args);
478+
}
479+
480+
let pathExpectedImage = resolve(this._storageByDeviceName, imageName);
481+
482+
if (!fileExists(pathExpectedImage)) {
483+
if (!this._storageByPlatform) {
484+
this._storageByPlatform = getStorageByPlatform(this._args);
485+
}
486+
pathExpectedImage = resolve(this._storageByPlatform, imageName);
487+
}
488+
489+
if (!fileExists(pathExpectedImage)) {
490+
pathExpectedImage = resolve(this._storageByDeviceName, imageName);
491+
}
492+
493+
return pathExpectedImage;
494+
}
460495
}

lib/image-helper.d.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@ export declare class ImageHelper {
66
private _cropImageRect;
77
private _blockOutAreas;
88
constructor(_args: INsCapabilities);
9-
readonly cropImageRect: IRectangle;
10-
cropImageRec: IRectangle;
9+
imageCropRect: IRectangle;
1110
blockOutAreas: IRectangle[];
1211
imageOutputLimit(): ImageOptions;
1312
thresholdType(): ImageOptions;
1413
threshold(): number;
1514
delta(): number;
16-
private static getOffsetPixels(args);
17-
static cropImageDefaultParams(_args: INsCapabilities): {
15+
static cropImageDefault(_args: INsCapabilities): {
1816
x: number;
1917
y: any;
18+
width: any;
19+
height: any;
2020
};
21+
private static getOffsetPixels(args);
2122
private runDiff(diffOptions, diffImage);
2223
compareImages(actual: string, expected: string, output: string, valueThreshold?: number, typeThreshold?: any): Promise<boolean>;
24+
clipRectangleImage(rect: IRectangle, path: string): Promise<{}>;
25+
readImage(path: string): Promise<any>;
2326
}

lib/image-helper.ts

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import * as blinkDiff from "blink-diff";
1+
import * as BlinkDiff from "blink-diff";
2+
import * as PngJsImage from "pngjs-image";
23
import { ImageOptions } from "./image-options";
34
import { INsCapabilities } from "./interfaces/ns-capabilities";
45
import { IRectangle } from "./interfaces/rectangle";
6+
import { Point } from "./point";
57

68
export class ImageHelper {
79

@@ -11,11 +13,11 @@ export class ImageHelper {
1113
constructor(private _args: INsCapabilities) {
1214
}
1315

14-
get cropImageRect() {
16+
get imageCropRect(): IRectangle {
1517
return this._cropImageRect;
1618
}
1719

18-
set cropImageRec(rect: IRectangle) {
20+
set imageCropRect(rect: IRectangle) {
1921
this._cropImageRect = rect;
2022
}
2123

@@ -43,15 +45,15 @@ export class ImageHelper {
4345
return 20;
4446
}
4547

46-
private static getOffsetPixels(args: INsCapabilities) {
47-
return args.device.config ? args.device.config.offsetPixels : 0
48+
public static cropImageDefault(_args: INsCapabilities) {
49+
return { x: 0, y: ImageHelper.getOffsetPixels(_args), width: undefined, height: undefined };
4850
}
4951

50-
public static cropImageDefaultParams(_args: INsCapabilities) {
51-
return { x: 0, y: ImageHelper.getOffsetPixels(_args) };
52+
private static getOffsetPixels(args: INsCapabilities) {
53+
return args.device.config ? args.device.config.offsetPixels : 0
5254
}
5355

54-
private runDiff(diffOptions: blinkDiff, diffImage: string) {
56+
private runDiff(diffOptions: BlinkDiff, diffImage: string) {
5557
return new Promise<boolean>((resolve, reject) => {
5658
diffOptions.run(function (error, result) {
5759
if (error) {
@@ -77,8 +79,7 @@ export class ImageHelper {
7779
}
7880

7981
public compareImages(actual: string, expected: string, output: string, valueThreshold: number = this.threshold(), typeThreshold: any = ImageOptions.pixel) {
80-
const rectToCrop = this._cropImageRect || ImageHelper.cropImageDefaultParams(this._args);
81-
let diff = new blinkDiff({
82+
const diff = new BlinkDiff({
8283

8384
imageAPath: actual,
8485
imageBPath: expected,
@@ -88,8 +89,8 @@ export class ImageHelper {
8889
threshold: valueThreshold,
8990
delta: this.delta(),
9091

91-
cropImageA: rectToCrop,
92-
cropImageB: rectToCrop,
92+
cropImageA: this._cropImageRect,
93+
cropImageB: this._cropImageRect,
9394
blockOut: this._blockOutAreas,
9495
verbose: this._args.verbose,
9596
});
@@ -98,4 +99,30 @@ export class ImageHelper {
9899
this._blockOutAreas = undefined;
99100
return result;
100101
}
102+
103+
public async clipRectangleImage(rect: IRectangle, path: string) {
104+
let imageToClip: PngJsImage;
105+
imageToClip = await this.readImage(path);
106+
imageToClip.clip(rect.x, rect.y, rect.width, rect.height);
107+
return new Promise((resolve, reject) => {
108+
imageToClip.writeImage(path, (err) => {
109+
if (err) {
110+
return reject(err);
111+
}
112+
return resolve();
113+
});
114+
115+
})
116+
}
117+
118+
public readImage(path: string): Promise<any> {
119+
return new Promise((resolve, reject) => {
120+
PngJsImage.readImage(path, (err, image) => {
121+
if (err) {
122+
return reject(err);
123+
}
124+
return resolve(image);
125+
});
126+
})
127+
}
101128
}

lib/ui-element.d.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export declare class UIElement {
1818
*/
1919
tap(): Promise<any>;
2020
/**
21-
* double tap
21+
* Double tap on element
2222
*/
2323
doubleTap(): Promise<any>;
2424
/**
@@ -29,6 +29,9 @@ export declare class UIElement {
2929
* Get size of element
3030
*/
3131
size(): Promise<Point>;
32+
/**
33+
* Get text of element
34+
*/
3235
text(): Promise<any>;
3336
/**
3437
* Get web driver element
@@ -52,7 +55,20 @@ export declare class UIElement {
5255
* @param wait
5356
*/
5457
waitForExist(wait?: number): Promise<any>;
58+
/**
59+
* Get attribute of element
60+
* @param attr
61+
*/
5562
getAttribute(attr: any): Promise<any>;
63+
/**
64+
* Get rectangle of element
65+
*/
66+
getRectangle(): Promise<{
67+
x: number;
68+
y: number;
69+
width: number;
70+
height: number;
71+
}>;
5672
/**
5773
* Scroll with offset from elemnt with minimum inertia
5874
* @param direction

0 commit comments

Comments
 (0)