@appium/images-plugin 2.0.10 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/lib/compare.d.ts +0 -4
- package/build/lib/compare.d.ts.map +1 -1
- package/build/lib/compare.js +12 -19
- package/build/lib/compare.js.map +1 -1
- package/build/lib/constants.d.ts +13 -0
- package/build/lib/constants.d.ts.map +1 -0
- package/build/lib/constants.js +59 -0
- package/build/lib/constants.js.map +1 -0
- package/build/lib/finder.d.ts +37 -48
- package/build/lib/finder.d.ts.map +1 -1
- package/build/lib/finder.js +167 -160
- package/build/lib/finder.js.map +1 -1
- package/build/lib/image-element.d.ts +60 -20
- package/build/lib/image-element.d.ts.map +1 -1
- package/build/lib/image-element.js +56 -34
- package/build/lib/image-element.js.map +1 -1
- package/build/lib/plugin.d.ts +3 -2
- package/build/lib/plugin.d.ts.map +1 -1
- package/build/lib/plugin.js +10 -12
- package/build/lib/plugin.js.map +1 -1
- package/lib/compare.js +6 -17
- package/lib/constants.js +70 -0
- package/lib/finder.js +186 -195
- package/lib/image-element.js +64 -31
- package/lib/plugin.js +10 -12
- package/package.json +4 -3
package/lib/finder.js
CHANGED
|
@@ -1,113 +1,72 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
2
|
import LRU from 'lru-cache';
|
|
3
3
|
import {errors} from 'appium/driver';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
ImageElement,
|
|
7
|
-
DEFAULT_TEMPLATE_IMAGE_SCALE,
|
|
8
|
-
IMAGE_EL_TAP_STRATEGY_W3C,
|
|
9
|
-
} from './image-element';
|
|
10
|
-
import {MATCH_TEMPLATE_MODE, compareImages, DEFAULT_MATCH_THRESHOLD} from './compare';
|
|
4
|
+
import {ImageElement} from './image-element';
|
|
5
|
+
import {compareImages} from './compare';
|
|
11
6
|
import log from './logger';
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_SETTINGS, MATCH_TEMPLATE_MODE, DEFAULT_TEMPLATE_IMAGE_SCALE,
|
|
9
|
+
DEFAULT_FIX_IMAGE_TEMPLATE_SCALE,
|
|
10
|
+
} from './constants';
|
|
11
|
+
import sharp from 'sharp';
|
|
12
12
|
|
|
13
|
-
const MJSONWP_ELEMENT_KEY = 'ELEMENT';
|
|
14
|
-
const W3C_ELEMENT_KEY = util.W3C_WEB_ELEMENT_IDENTIFIER;
|
|
15
|
-
const DEFAULT_FIX_IMAGE_TEMPLATE_SCALE = 1;
|
|
16
13
|
// Used to compare ratio and screen width
|
|
17
14
|
// Pixel is basically under 1080 for example. 100K is probably enough fo a while.
|
|
18
15
|
const FLOAT_PRECISION = 100000;
|
|
19
16
|
const MAX_CACHE_ITEMS = 100;
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
fixImageFindScreenshotDims: true,
|
|
36
|
-
|
|
37
|
-
// whether Appium should ensure that an image template sent in during image
|
|
38
|
-
// element find should have its size adjusted so the match algorithm will not
|
|
39
|
-
// complain
|
|
40
|
-
fixImageTemplateSize: false,
|
|
41
|
-
|
|
42
|
-
// whether Appium should ensure that an image template sent in during image
|
|
43
|
-
// element find should have its scale adjusted to display size so the match
|
|
44
|
-
// algorithm will not complain.
|
|
45
|
-
// e.g. iOS has `width=375, height=667` window rect, but its screenshot is
|
|
46
|
-
// `width=750 × height=1334` pixels. This setting help to adjust the scale
|
|
47
|
-
// if a user use `width=750 × height=1334` pixels's base template image.
|
|
48
|
-
fixImageTemplateScale: false,
|
|
49
|
-
|
|
50
|
-
// Users might have scaled template image to reduce their storage size.
|
|
51
|
-
// This setting allows users to scale a template image they send to Appium server
|
|
52
|
-
// so that the Appium server compares the actual scale users originally had.
|
|
53
|
-
// e.g. If a user has an image of 270 x 32 pixels which was originally 1080 x 126 pixels,
|
|
54
|
-
// the user can set {defaultImageTemplateScale: 4.0} to scale the small image
|
|
55
|
-
// to the original one so that Appium can compare it as the original one.
|
|
56
|
-
defaultImageTemplateScale: DEFAULT_TEMPLATE_IMAGE_SCALE,
|
|
57
|
-
|
|
58
|
-
// whether Appium should re-check that an image element can be matched
|
|
59
|
-
// against the current screenshot before clicking it
|
|
60
|
-
checkForImageElementStaleness: true,
|
|
61
|
-
|
|
62
|
-
// whether before clicking on an image element Appium should re-determine the
|
|
63
|
-
// position of the element on screen
|
|
64
|
-
autoUpdateImageElementPosition: false,
|
|
65
|
-
|
|
66
|
-
// which method to use for tapping by coordinate for image elements. the
|
|
67
|
-
// options are 'w3c' or 'mjsonwp'
|
|
68
|
-
imageElementTapStrategy: IMAGE_EL_TAP_STRATEGY_W3C,
|
|
69
|
-
|
|
70
|
-
// which method to use to save the matched image area in ImageElement class.
|
|
71
|
-
// It is used for debugging purpose.
|
|
72
|
-
getMatchedImageResult: false,
|
|
73
|
-
};
|
|
17
|
+
const MAX_CACHE_AGE_MS = 24 * 60 * 60 * 1000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checks if one rect fully contains another
|
|
21
|
+
*
|
|
22
|
+
* @param {import('@appium/types').Rect} templateRect The bounding rect
|
|
23
|
+
* @param {import('@appium/types').Rect} rect The rect to be checked for containment
|
|
24
|
+
* @returns {boolean} True if templateRect contains rect
|
|
25
|
+
*/
|
|
26
|
+
function containsRect(templateRect, rect) {
|
|
27
|
+
return templateRect.x <= rect.x && templateRect.y <= rect.y
|
|
28
|
+
&& rect.width <= templateRect.x + templateRect.width - rect.x
|
|
29
|
+
&& rect.height <= templateRect.y + templateRect.height - rect.y;
|
|
30
|
+
}
|
|
31
|
+
|
|
74
32
|
const NO_OCCURRENCES_PATTERN = /Cannot find any occurrences/;
|
|
75
33
|
const CONDITION_UNMET_PATTERN = /Condition unmet/;
|
|
76
34
|
|
|
77
35
|
|
|
78
36
|
export default class ImageElementFinder {
|
|
79
|
-
/** @type {ExternalDriver} */
|
|
80
|
-
driver;
|
|
81
|
-
|
|
82
37
|
/** @type {LRU<string,ImageElement>} */
|
|
83
|
-
|
|
38
|
+
_imgElCache;
|
|
84
39
|
|
|
85
40
|
/**
|
|
86
|
-
*
|
|
87
|
-
* @param {ExternalDriver} driver
|
|
88
|
-
* @param {number} [maxSize]
|
|
41
|
+
* @param {number} max
|
|
89
42
|
*/
|
|
90
|
-
constructor(
|
|
91
|
-
this.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
sizeCalculation: (el) => el.template.length,
|
|
43
|
+
constructor(max = MAX_CACHE_ITEMS) {
|
|
44
|
+
this._imgElCache = new LRU({
|
|
45
|
+
ttl: MAX_CACHE_AGE_MS,
|
|
46
|
+
updateAgeOnGet: true,
|
|
47
|
+
max,
|
|
96
48
|
});
|
|
97
49
|
}
|
|
98
50
|
|
|
99
|
-
setDriver(driver) {
|
|
100
|
-
this.driver = driver;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
51
|
/**
|
|
104
52
|
* @param {ImageElement} imgEl
|
|
105
53
|
* @returns {Element}
|
|
106
54
|
*/
|
|
107
55
|
registerImageElement(imgEl) {
|
|
108
|
-
this.
|
|
109
|
-
|
|
110
|
-
|
|
56
|
+
this._imgElCache.set(imgEl.id, imgEl);
|
|
57
|
+
return imgEl.asElement();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {string} imgElId
|
|
62
|
+
* @returns {ImageElement|undefined}
|
|
63
|
+
*/
|
|
64
|
+
getImageElement(imgElId) {
|
|
65
|
+
return this._imgElCache.get(imgElId);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
clearImageElements() {
|
|
69
|
+
this._imgElCache.clear();
|
|
111
70
|
}
|
|
112
71
|
|
|
113
72
|
/**
|
|
@@ -118,27 +77,28 @@ export default class ImageElementFinder {
|
|
|
118
77
|
* multiple
|
|
119
78
|
* @property {boolean} [ignoreDefaultImageTemplateScale=false] - Whether we
|
|
120
79
|
* ignore defaultImageTemplateScale. It can be used when you would like to
|
|
121
|
-
* scale
|
|
80
|
+
* scale template with defaultImageTemplateScale setting.
|
|
81
|
+
* @property {import('@appium/types').Rect?} containerRect - The bounding
|
|
82
|
+
* rectangle to limit the search in
|
|
122
83
|
*/
|
|
123
84
|
|
|
124
85
|
/**
|
|
125
86
|
* Find a screen rect represented by an ImageElement corresponding to an image
|
|
126
87
|
* template sent in by the client
|
|
127
88
|
*
|
|
128
|
-
* @param {
|
|
89
|
+
* @param {Buffer} template - image used as a template to be
|
|
129
90
|
* matched in the screenshot
|
|
91
|
+
* @param {ExternalDriver} driver
|
|
130
92
|
* @param {FindByImageOptions} opts - additional options
|
|
131
93
|
*
|
|
132
94
|
* @returns {Promise<Element|Element[]|ImageElement>} - WebDriver element with a special id prefix
|
|
133
95
|
*/
|
|
134
96
|
async findByImage(
|
|
135
|
-
|
|
136
|
-
|
|
97
|
+
template,
|
|
98
|
+
driver,
|
|
99
|
+
{shouldCheckStaleness = false, multiple = false, ignoreDefaultImageTemplateScale = false, containerRect = null}
|
|
137
100
|
) {
|
|
138
|
-
|
|
139
|
-
throw new Error(`Can't find without a driver!`);
|
|
140
|
-
}
|
|
141
|
-
const settings = {...DEFAULT_SETTINGS, ...this.driver.settings.getSettings()};
|
|
101
|
+
const settings = {...DEFAULT_SETTINGS, ...driver.settings.getSettings()};
|
|
142
102
|
const {
|
|
143
103
|
imageMatchThreshold: threshold,
|
|
144
104
|
imageMatchMethod,
|
|
@@ -149,28 +109,38 @@ export default class ImageElementFinder {
|
|
|
149
109
|
} = settings;
|
|
150
110
|
|
|
151
111
|
log.info(`Finding image element with match threshold ${threshold}`);
|
|
152
|
-
if (!
|
|
153
|
-
throw new Error("This driver does not support the required '
|
|
112
|
+
if (!driver.getWindowRect && !driver.getWindowSize) {
|
|
113
|
+
throw new Error("This driver does not support the required 'getWindowRect' command");
|
|
114
|
+
}
|
|
115
|
+
let screenSize;
|
|
116
|
+
if (driver.getWindowRect) {
|
|
117
|
+
const screenRect = await driver.getWindowRect();
|
|
118
|
+
screenSize = {
|
|
119
|
+
width: screenRect.width,
|
|
120
|
+
height: screenRect.height,
|
|
121
|
+
};
|
|
122
|
+
} else {
|
|
123
|
+
// TODO: Drop the deprecated endpoint
|
|
124
|
+
screenSize = await driver.getWindowSize();
|
|
154
125
|
}
|
|
155
|
-
const {width: screenWidth, height: screenHeight} = await this.driver.getWindowSize();
|
|
156
126
|
|
|
157
127
|
// someone might have sent in a template that's larger than the screen
|
|
158
128
|
// dimensions. If so let's check and cut it down to size since the algorithm
|
|
159
129
|
// will not work unless we do. But because it requires some potentially
|
|
160
130
|
// expensive commands, only do this if the user has requested it in settings.
|
|
161
131
|
if (fixImageTemplateSize) {
|
|
162
|
-
|
|
132
|
+
template = await this.ensureTemplateSize(template, {
|
|
133
|
+
width: containerRect ? containerRect.width : screenSize.width,
|
|
134
|
+
height: containerRect ? containerRect.height : screenSize.height,
|
|
135
|
+
});
|
|
163
136
|
}
|
|
164
137
|
|
|
165
138
|
const results = [];
|
|
166
139
|
const condition = async () => {
|
|
167
140
|
try {
|
|
168
|
-
const {
|
|
169
|
-
screenWidth,
|
|
170
|
-
screenHeight
|
|
171
|
-
);
|
|
141
|
+
const {screenshot, scale} = await this.getScreenshotForImageFind(driver, screenSize);
|
|
172
142
|
|
|
173
|
-
|
|
143
|
+
template = await this.fixImageTemplateScale(template, {
|
|
174
144
|
defaultImageTemplateScale,
|
|
175
145
|
ignoreDefaultImageTemplateScale,
|
|
176
146
|
fixImageTemplateScale,
|
|
@@ -185,21 +155,21 @@ export default class ImageElementFinder {
|
|
|
185
155
|
if (imageMatchMethod) {
|
|
186
156
|
comparisonOpts.method = imageMatchMethod;
|
|
187
157
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return
|
|
158
|
+
|
|
159
|
+
const pushIfOk = (el) => {
|
|
160
|
+
if (containerRect && !containsRect(containerRect, el.rect)) {
|
|
161
|
+
log.debug(
|
|
162
|
+
`The matched element rectangle ${JSON.stringify(el.rect)} is not located ` +
|
|
163
|
+
`inside of the bounding rectangle ${JSON.stringify(containerRect)}, thus rejected`
|
|
164
|
+
);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
results.push(el);
|
|
168
|
+
return true;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const elOrEls = await compareImages(MATCH_TEMPLATE_MODE, screenshot, template, comparisonOpts);
|
|
172
|
+
return _.some((_.isArray(elOrEls) ? elOrEls : [elOrEls]).map(pushIfOk));
|
|
203
173
|
} catch (err) {
|
|
204
174
|
// if compareImages fails, we'll get a specific error, but we should
|
|
205
175
|
// retry, so trap that and just return false to trigger the next round of
|
|
@@ -213,7 +183,7 @@ export default class ImageElementFinder {
|
|
|
213
183
|
};
|
|
214
184
|
|
|
215
185
|
try {
|
|
216
|
-
await
|
|
186
|
+
await driver.implicitWaitForCondition(condition);
|
|
217
187
|
} catch (err) {
|
|
218
188
|
// this `implicitWaitForCondition` method will throw a 'Condition unmet'
|
|
219
189
|
// error if an element is not found eventually. In that case, we will
|
|
@@ -235,7 +205,14 @@ export default class ImageElementFinder {
|
|
|
235
205
|
|
|
236
206
|
const elements = results.map(({rect, score, visualization}) => {
|
|
237
207
|
log.info(`Image template matched: ${JSON.stringify(rect)}`);
|
|
238
|
-
return new ImageElement(
|
|
208
|
+
return new ImageElement({
|
|
209
|
+
template,
|
|
210
|
+
rect,
|
|
211
|
+
score,
|
|
212
|
+
match: visualization ? Buffer.from(visualization, 'base64') : null,
|
|
213
|
+
finder: this,
|
|
214
|
+
containerRect,
|
|
215
|
+
});
|
|
239
216
|
});
|
|
240
217
|
|
|
241
218
|
// if we're just checking staleness, return straightaway so we don't add
|
|
@@ -253,86 +230,90 @@ export default class ImageElementFinder {
|
|
|
253
230
|
/**
|
|
254
231
|
* Ensure that the image template sent in for a find is of a suitable size
|
|
255
232
|
*
|
|
256
|
-
* @param {
|
|
257
|
-
* @param {
|
|
258
|
-
* @param {number} screenHeight - height of screen
|
|
233
|
+
* @param {Buffer} template - template image
|
|
234
|
+
* @param {import('@appium/types').Size} maxSize - size of the bounding rectangle
|
|
259
235
|
*
|
|
260
|
-
* @returns {Promise<
|
|
236
|
+
* @returns {Promise<Buffer>} image, potentially resized
|
|
261
237
|
*/
|
|
262
|
-
async ensureTemplateSize(
|
|
263
|
-
|
|
264
|
-
|
|
238
|
+
async ensureTemplateSize(template, maxSize) {
|
|
239
|
+
const imgObj = sharp(template);
|
|
240
|
+
const {width: tplWidth, height: tplHeight} = await imgObj.metadata();
|
|
265
241
|
|
|
266
242
|
log.info(
|
|
267
|
-
`Template image is ${tplWidth}x${tplHeight}.
|
|
243
|
+
`Template image is ${tplWidth}x${tplHeight}. Bounding rectangle size is ${maxSize.width}x${maxSize.height}`
|
|
268
244
|
);
|
|
269
245
|
// if the template fits inside the screen dimensions, we're good
|
|
270
|
-
if (tplWidth <=
|
|
271
|
-
return
|
|
246
|
+
if (tplWidth <= maxSize.width && tplHeight <= maxSize.height) {
|
|
247
|
+
return template;
|
|
272
248
|
}
|
|
273
249
|
|
|
274
250
|
log.info(
|
|
275
251
|
`Scaling template image from ${tplWidth}x${tplHeight} to match ` +
|
|
276
|
-
|
|
252
|
+
`the bounding rectangle at ${maxSize.width}x${maxSize.height}`
|
|
277
253
|
);
|
|
278
|
-
// otherwise, scale it to fit inside the
|
|
279
|
-
|
|
280
|
-
return
|
|
254
|
+
// otherwise, scale it to fit inside the bounding rectangle dimensions:
|
|
255
|
+
// https://sharp.pixelplumbing.com/api-resize
|
|
256
|
+
return await imgObj.resize({
|
|
257
|
+
width: parseInt(maxSize.width, 10),
|
|
258
|
+
height: parseInt(maxSize.height, 10),
|
|
259
|
+
fit: 'inside',
|
|
260
|
+
})
|
|
261
|
+
.toBuffer();
|
|
281
262
|
}
|
|
282
263
|
|
|
283
264
|
/**
|
|
284
265
|
* Get the screenshot image that will be used for find by element, potentially
|
|
285
266
|
* altering it in various ways based on user-requested settings
|
|
286
267
|
*
|
|
287
|
-
* @param {
|
|
288
|
-
* @param {
|
|
268
|
+
* @param {ExternalDriver} driver
|
|
269
|
+
* @param {import('@appium/types').Size} screenSize - The original size of the screen
|
|
289
270
|
*
|
|
290
|
-
* @returns {Promise<Screenshot & {scale?: ScreenshotScale}>}
|
|
271
|
+
* @returns {Promise<Screenshot & {scale?: ScreenshotScale}>} PNG screenshot and ScreenshotScale
|
|
291
272
|
*/
|
|
292
|
-
async getScreenshotForImageFind(
|
|
293
|
-
if (!
|
|
273
|
+
async getScreenshotForImageFind(driver, screenSize) {
|
|
274
|
+
if (!driver.getScreenshot) {
|
|
294
275
|
throw new Error("This driver does not support the required 'getScreenshot' command");
|
|
295
276
|
}
|
|
296
|
-
const settings = Object.assign({}, DEFAULT_SETTINGS,
|
|
277
|
+
const settings = Object.assign({}, DEFAULT_SETTINGS, driver.settings.getSettings());
|
|
297
278
|
const {fixImageFindScreenshotDims} = settings;
|
|
298
279
|
|
|
299
|
-
|
|
280
|
+
const screenshot = Buffer.from(await driver.getScreenshot(), 'base64');
|
|
300
281
|
|
|
301
282
|
// if the user has requested not to correct for aspect or size differences
|
|
302
283
|
// between the screenshot and the screen, just return the screenshot now
|
|
303
284
|
if (!fixImageFindScreenshotDims) {
|
|
304
285
|
log.info(`Not verifying screenshot dimensions match screen`);
|
|
305
|
-
return {
|
|
286
|
+
return {screenshot};
|
|
306
287
|
}
|
|
307
288
|
|
|
308
|
-
if (
|
|
289
|
+
if (screenSize.width < 1 || screenSize.height < 1) {
|
|
309
290
|
log.warn(
|
|
310
|
-
`The retrieved screen size ${
|
|
311
|
-
|
|
291
|
+
`The retrieved screen size ${screenSize.width}x${screenSize.height} does ` +
|
|
292
|
+
`not seem to be valid. No changes will be applied to the screenshot`
|
|
312
293
|
);
|
|
313
|
-
return {
|
|
294
|
+
return {screenshot};
|
|
314
295
|
}
|
|
315
296
|
|
|
316
297
|
// otherwise, do some verification on the screenshot to make sure it matches
|
|
317
298
|
// the screen size and aspect ratio
|
|
318
299
|
log.info('Verifying screenshot size and aspect ratio');
|
|
319
300
|
|
|
320
|
-
let imgObj =
|
|
321
|
-
let {width: shotWidth, height: shotHeight} = imgObj.
|
|
301
|
+
let imgObj = sharp(screenshot);
|
|
302
|
+
let {width: shotWidth, height: shotHeight} = await imgObj.metadata();
|
|
322
303
|
|
|
323
304
|
if (shotWidth < 1 || shotHeight < 1) {
|
|
324
305
|
log.warn(
|
|
325
306
|
`The retrieved screenshot size ${shotWidth}x${shotHeight} does ` +
|
|
326
|
-
|
|
307
|
+
`not seem to be valid. No changes will be applied to the screenshot`
|
|
327
308
|
);
|
|
328
|
-
return {
|
|
309
|
+
return {screenshot};
|
|
329
310
|
}
|
|
330
311
|
|
|
331
|
-
if (
|
|
312
|
+
if (screenSize.width === shotWidth && screenSize.height === shotHeight) {
|
|
332
313
|
// the height and width of the screenshot and the device screen match, which
|
|
333
314
|
// means we should be safe when doing template matches
|
|
334
315
|
log.info('Screenshot size matched screen size');
|
|
335
|
-
return {
|
|
316
|
+
return {screenshot};
|
|
336
317
|
}
|
|
337
318
|
|
|
338
319
|
// otherwise, if they don't match, it could spell problems for the accuracy
|
|
@@ -343,19 +324,19 @@ export default class ImageElementFinder {
|
|
|
343
324
|
|
|
344
325
|
const scale = {xScale: 1.0, yScale: 1.0};
|
|
345
326
|
|
|
346
|
-
const screenAR =
|
|
327
|
+
const screenAR = screenSize.width / screenSize.height;
|
|
347
328
|
const shotAR = shotWidth / shotHeight;
|
|
348
329
|
if (Math.round(screenAR * FLOAT_PRECISION) === Math.round(shotAR * FLOAT_PRECISION)) {
|
|
349
330
|
log.info(
|
|
350
331
|
`Screenshot aspect ratio '${shotAR}' (${shotWidth}x${shotHeight}) matched ` +
|
|
351
|
-
|
|
332
|
+
`screen aspect ratio '${screenAR}' (${screenSize.width}x${screenSize.height})`
|
|
352
333
|
);
|
|
353
334
|
} else {
|
|
354
335
|
log.warn(
|
|
355
336
|
`When trying to find an element, determined that the screen ` +
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
337
|
+
`aspect ratio and screenshot aspect ratio are different. Screen ` +
|
|
338
|
+
`is ${screenSize.width}x${screenSize.height} whereas screenshot is ` +
|
|
339
|
+
`${shotWidth}x${shotHeight}.`
|
|
359
340
|
);
|
|
360
341
|
|
|
361
342
|
// In the case where the x-scale and y-scale are different, we need to decide
|
|
@@ -364,46 +345,56 @@ export default class ImageElementFinder {
|
|
|
364
345
|
// this.getScreenshot(shotWidth, shotHeight) is 540x397,
|
|
365
346
|
// this.getDeviceSize(screenWidth, screenHeight) is 1080x1920.
|
|
366
347
|
// The ratio would then be {xScale: 0.5, yScale: 0.2}.
|
|
367
|
-
// In this case, we must
|
|
348
|
+
// In this case, we must use `yScale: 0.2` as scaleFactor, because
|
|
368
349
|
// if we select the xScale, the height will be bigger than real screenshot size
|
|
369
350
|
// which is used to image comparison by OpenCV as a base image.
|
|
370
351
|
// All of this is primarily useful when the screenshot is a horizontal slice taken out of the
|
|
371
352
|
// screen (for example not including top/bottom nav bars)
|
|
372
|
-
const xScale = (1.0 * shotWidth) /
|
|
373
|
-
const yScale = (1.0 * shotHeight) /
|
|
374
|
-
const scaleFactor = xScale
|
|
353
|
+
const xScale = (1.0 * shotWidth) / screenSize.width;
|
|
354
|
+
const yScale = (1.0 * shotHeight) / screenSize.height;
|
|
355
|
+
const scaleFactor = Math.min(xScale, yScale);
|
|
356
|
+
const [newWidth, newHeight] = [shotWidth * scaleFactor, shotHeight * scaleFactor]
|
|
357
|
+
.map((x) => parseInt(x, 10));
|
|
375
358
|
|
|
376
359
|
log.warn(
|
|
377
|
-
`Resizing screenshot to ${
|
|
378
|
-
|
|
379
|
-
|
|
360
|
+
`Resizing screenshot to ${newWidth}x${newHeight} to match ` +
|
|
361
|
+
`screen aspect ratio so that image element coordinates have a ` +
|
|
362
|
+
`greater chance of being correct.`
|
|
380
363
|
);
|
|
381
|
-
imgObj = imgObj.resize(
|
|
364
|
+
imgObj = imgObj.resize({
|
|
365
|
+
width: newWidth,
|
|
366
|
+
height: newHeight,
|
|
367
|
+
fit: 'fill',
|
|
368
|
+
});
|
|
382
369
|
|
|
383
370
|
scale.xScale *= scaleFactor;
|
|
384
371
|
scale.yScale *= scaleFactor;
|
|
385
|
-
|
|
386
|
-
shotWidth = imgObj.bitmap.width;
|
|
387
|
-
shotHeight = imgObj.bitmap.height;
|
|
372
|
+
[shotWidth, shotHeight] = [newWidth, newHeight];
|
|
388
373
|
}
|
|
389
374
|
|
|
390
375
|
// Resize based on the screen dimensions only if both width and height are mismatched
|
|
391
376
|
// since except for that, it might be a situation which is different window rect and
|
|
392
377
|
// screenshot size like `@driver.window_rect #=>x=0, y=0, width=1080, height=1794` and
|
|
393
378
|
// `"deviceScreenSize"=>"1080x1920"`
|
|
394
|
-
if (
|
|
379
|
+
if (screenSize.width !== shotWidth && screenSize.height !== shotHeight) {
|
|
395
380
|
log.info(
|
|
396
381
|
`Scaling screenshot from ${shotWidth}x${shotHeight} to match ` +
|
|
397
|
-
|
|
382
|
+
`screen at ${screenSize.width}x${screenSize.height}`
|
|
398
383
|
);
|
|
399
|
-
imgObj = imgObj.resize(
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
384
|
+
imgObj = imgObj.resize({
|
|
385
|
+
width: parseInt(screenSize.width, 10),
|
|
386
|
+
height: parseInt(screenSize.height, 10),
|
|
387
|
+
fit: 'fill',
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
scale.xScale *= (1.0 * screenSize.width) / shotWidth;
|
|
391
|
+
scale.yScale *= (1.0 * screenSize.height) / shotHeight;
|
|
403
392
|
}
|
|
404
393
|
|
|
405
|
-
|
|
406
|
-
|
|
394
|
+
return {
|
|
395
|
+
screenshot: await imgObj.toBuffer(),
|
|
396
|
+
scale,
|
|
397
|
+
};
|
|
407
398
|
}
|
|
408
399
|
|
|
409
400
|
/**
|
|
@@ -411,7 +402,7 @@ export default class ImageElementFinder {
|
|
|
411
402
|
* @property {boolean} fixImageTemplateScale - fixImageTemplateScale in device-settings
|
|
412
403
|
* @property {number} defaultImageTemplateScale - defaultImageTemplateScale in device-settings
|
|
413
404
|
* @property {boolean} ignoreDefaultImageTemplateScale - Ignore defaultImageTemplateScale if it has true.
|
|
414
|
-
* If
|
|
405
|
+
* If the template has been scaled to defaultImageTemplateScale or should ignore the scale,
|
|
415
406
|
* this parameter should be true. e.g. click in image-element module
|
|
416
407
|
* @property {number} xScale - Scale ratio for width
|
|
417
408
|
* @property {number} yScale - Scale ratio for height
|
|
@@ -421,20 +412,19 @@ export default class ImageElementFinder {
|
|
|
421
412
|
* Get a image that will be used for template maching.
|
|
422
413
|
* Returns scaled image if scale ratio is provided.
|
|
423
414
|
*
|
|
424
|
-
*
|
|
425
|
-
* @param {string} b64Template - base64-encoded image used as a template to be
|
|
415
|
+
* @param {Buffer} template - image used as a template to be
|
|
426
416
|
* matched in the screenshot
|
|
427
417
|
* @param {ImageTemplateSettings} opts - Image template scale related options
|
|
428
418
|
*
|
|
429
|
-
* @returns {Promise<
|
|
419
|
+
* @returns {Promise<Buffer>} scaled template screenshot
|
|
430
420
|
*/
|
|
431
|
-
async fixImageTemplateScale(
|
|
421
|
+
async fixImageTemplateScale(template, opts) {
|
|
432
422
|
if (!opts) {
|
|
433
|
-
return
|
|
423
|
+
return template;
|
|
434
424
|
}
|
|
435
425
|
|
|
436
426
|
let {
|
|
437
|
-
fixImageTemplateScale = false,
|
|
427
|
+
fixImageTemplateScale: fixTplScale = false,
|
|
438
428
|
defaultImageTemplateScale = DEFAULT_TEMPLATE_IMAGE_SCALE,
|
|
439
429
|
ignoreDefaultImageTemplateScale = false,
|
|
440
430
|
xScale = DEFAULT_FIX_IMAGE_TEMPLATE_SCALE,
|
|
@@ -446,12 +436,12 @@ export default class ImageElementFinder {
|
|
|
446
436
|
}
|
|
447
437
|
|
|
448
438
|
// Default
|
|
449
|
-
if (defaultImageTemplateScale === DEFAULT_TEMPLATE_IMAGE_SCALE && !
|
|
450
|
-
return
|
|
439
|
+
if (defaultImageTemplateScale === DEFAULT_TEMPLATE_IMAGE_SCALE && !fixTplScale) {
|
|
440
|
+
return template;
|
|
451
441
|
}
|
|
452
442
|
|
|
453
443
|
// Calculate xScale and yScale Appium should scale
|
|
454
|
-
if (
|
|
444
|
+
if (fixTplScale) {
|
|
455
445
|
xScale *= defaultImageTemplateScale;
|
|
456
446
|
yScale *= defaultImageTemplateScale;
|
|
457
447
|
} else {
|
|
@@ -460,7 +450,7 @@ export default class ImageElementFinder {
|
|
|
460
450
|
|
|
461
451
|
// xScale and yScale can be NaN if defaultImageTemplateScale is string, for example
|
|
462
452
|
if (!parseFloat(String(xScale)) || !parseFloat(String(yScale))) {
|
|
463
|
-
return
|
|
453
|
+
return template;
|
|
464
454
|
}
|
|
465
455
|
|
|
466
456
|
// Return if the scale is default, 1, value
|
|
@@ -474,26 +464,27 @@ export default class ImageElementFinder {
|
|
|
474
464
|
)
|
|
475
465
|
)
|
|
476
466
|
) {
|
|
477
|
-
return
|
|
467
|
+
return template;
|
|
478
468
|
}
|
|
479
469
|
|
|
480
|
-
let
|
|
481
|
-
|
|
470
|
+
let imgObj = sharp(template);
|
|
471
|
+
const {width: baseTempWidth, height: baseTempHeigh} = await imgObj.metadata();
|
|
482
472
|
|
|
483
473
|
const scaledWidth = baseTempWidth * xScale;
|
|
484
474
|
const scaledHeight = baseTempHeigh * yScale;
|
|
485
475
|
log.info(
|
|
486
|
-
`Scaling template image from ${baseTempWidth}x${baseTempHeigh}`
|
|
487
|
-
` to ${scaledWidth}x${scaledHeight}`
|
|
476
|
+
`Scaling template image from ${baseTempWidth}x${baseTempHeigh} to ${scaledWidth}x${scaledHeight}`
|
|
488
477
|
);
|
|
489
478
|
log.info(`The ratio is ${xScale} and ${yScale}`);
|
|
490
|
-
|
|
491
|
-
|
|
479
|
+
imgObj = imgObj.resize({
|
|
480
|
+
width: parseInt(scaledWidth, 10),
|
|
481
|
+
height: parseInt(scaledHeight, 10),
|
|
482
|
+
fit: 'fill',
|
|
483
|
+
});
|
|
484
|
+
return await imgObj.toBuffer();
|
|
492
485
|
}
|
|
493
486
|
}
|
|
494
487
|
|
|
495
|
-
export {W3C_ELEMENT_KEY, MJSONWP_ELEMENT_KEY, DEFAULT_SETTINGS, DEFAULT_FIX_IMAGE_TEMPLATE_SCALE};
|
|
496
|
-
|
|
497
488
|
/**
|
|
498
489
|
* @typedef {import('@appium/types').ExternalDriver} ExternalDriver
|
|
499
490
|
* @typedef {import('@appium/types').Element} Element
|
|
@@ -501,7 +492,7 @@ export {W3C_ELEMENT_KEY, MJSONWP_ELEMENT_KEY, DEFAULT_SETTINGS, DEFAULT_FIX_IMAG
|
|
|
501
492
|
|
|
502
493
|
/**
|
|
503
494
|
* @typedef Screenshot
|
|
504
|
-
* @property {
|
|
495
|
+
* @property {Buffer} screenshot - screenshot image as PNG
|
|
505
496
|
*/
|
|
506
497
|
|
|
507
498
|
/**
|