@appium/images-plugin 1.2.3 → 1.3.2

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/lib/finder.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import _ from 'lodash';
2
2
  import LRU from 'lru-cache';
3
- import { imageUtil, util } from '@appium/support';
4
- import { errors } from '@appium/base-driver';
5
- import { ImageElement, DEFAULT_TEMPLATE_IMAGE_SCALE,
6
- IMAGE_EL_TAP_STRATEGY_W3C } from './image-element';
7
- import { MATCH_TEMPLATE_MODE, compareImages, DEFAULT_MATCH_THRESHOLD } from './compare';
3
+ import {errors} from 'appium/driver';
4
+ import {util, imageUtil} from 'appium/support';
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';
8
11
  import log from './logger';
9
12
 
10
13
  const MJSONWP_ELEMENT_KEY = 'ELEMENT';
@@ -13,7 +16,8 @@ const DEFAULT_FIX_IMAGE_TEMPLATE_SCALE = 1;
13
16
  // Used to compare ratio and screen width
14
17
  // Pixel is basically under 1080 for example. 100K is probably enough fo a while.
15
18
  const FLOAT_PRECISION = 100000;
16
- const MAX_CACHE_SIZE = 1024 * 1024 * 40; // 40mb
19
+ const MAX_CACHE_ITEMS = 100;
20
+ const MAX_CACHE_SIZE_BYTES = 1024 * 1024 * 40; // 40mb
17
21
 
18
22
  const DEFAULT_SETTINGS = {
19
23
  // value between 0 and 1 representing match strength, below which an image
@@ -69,26 +73,42 @@ const DEFAULT_SETTINGS = {
69
73
  };
70
74
 
71
75
  export default class ImageElementFinder {
72
- constructor (driver, max = MAX_CACHE_SIZE) {
76
+ /** @type {ExternalDriver} */
77
+ driver;
78
+
79
+ /** @type {LRU<string,ImageElement>} */
80
+ imgElCache;
81
+
82
+ /**
83
+ *
84
+ * @param {ExternalDriver} driver
85
+ * @param {number} [maxSize]
86
+ */
87
+ constructor(driver, maxSize = MAX_CACHE_SIZE_BYTES) {
73
88
  this.driver = driver;
74
89
  this.imgElCache = new LRU({
75
- max,
76
- length: (el) => el.template.length,
90
+ max: MAX_CACHE_ITEMS,
91
+ maxSize,
92
+ sizeCalculation: (el) => el.template.length,
77
93
  });
78
94
  }
79
95
 
80
- setDriver (driver) {
96
+ setDriver(driver) {
81
97
  this.driver = driver;
82
98
  }
83
99
 
84
- registerImageElement (imgEl) {
100
+ /**
101
+ * @param {ImageElement} imgEl
102
+ * @returns {Element}
103
+ */
104
+ registerImageElement(imgEl) {
85
105
  this.imgElCache.set(imgEl.id, imgEl);
86
106
  const protoKey = this.driver.isW3CProtocol() ? W3C_ELEMENT_KEY : MJSONWP_ELEMENT_KEY;
87
107
  return imgEl.asElement(protoKey);
88
108
  }
89
109
 
90
110
  /**
91
- * @typedef {Object} FindByImageOptions
111
+ * @typedef FindByImageOptions
92
112
  * @property {boolean} [shouldCheckStaleness=false] - whether this call to find an
93
113
  * image is merely to check staleness. If so we can bypass a lot of logic
94
114
  * @property {boolean} [multiple=false] - Whether we are finding one element or
@@ -104,26 +124,25 @@ export default class ImageElementFinder {
104
124
  *
105
125
  * @param {string} b64Template - base64-encoded image used as a template to be
106
126
  * matched in the screenshot
107
- * @param {FindByImageOptions} - additional options
127
+ * @param {FindByImageOptions} opts - additional options
108
128
  *
109
- * @returns {WebElement} - WebDriver element with a special id prefix
129
+ * @returns {Promise<Element|Element[]|ImageElement>} - WebDriver element with a special id prefix
110
130
  */
111
- async findByImage (b64Template, {
112
- shouldCheckStaleness = false,
113
- multiple = false,
114
- ignoreDefaultImageTemplateScale = false,
115
- }) {
131
+ async findByImage(
132
+ b64Template,
133
+ {shouldCheckStaleness = false, multiple = false, ignoreDefaultImageTemplateScale = false}
134
+ ) {
116
135
  if (!this.driver) {
117
136
  throw new Error(`Can't find without a driver!`);
118
137
  }
119
- const settings = Object.assign({}, DEFAULT_SETTINGS, this.driver.settings.getSettings());
138
+ const settings = {...DEFAULT_SETTINGS, ...this.driver.settings.getSettings()};
120
139
  const {
121
140
  imageMatchThreshold: threshold,
122
141
  imageMatchMethod,
123
142
  fixImageTemplateSize,
124
143
  fixImageTemplateScale,
125
144
  defaultImageTemplateScale,
126
- getMatchedImageResult: visualize
145
+ getMatchedImageResult: visualize,
127
146
  } = settings;
128
147
 
129
148
  log.info(`Finding image element with match threshold ${threshold}`);
@@ -137,18 +156,22 @@ export default class ImageElementFinder {
137
156
  // will not work unless we do. But because it requires some potentially
138
157
  // expensive commands, only do this if the user has requested it in settings.
139
158
  if (fixImageTemplateSize) {
140
- b64Template = await this.ensureTemplateSize(b64Template, screenWidth,
141
- screenHeight);
159
+ b64Template = await this.ensureTemplateSize(b64Template, screenWidth, screenHeight);
142
160
  }
143
161
 
144
162
  const results = [];
145
163
  const condition = async () => {
146
164
  try {
147
- const {b64Screenshot, scale} = await this.getScreenshotForImageFind(screenWidth, screenHeight);
165
+ const {b64Screenshot, scale} = await this.getScreenshotForImageFind(
166
+ screenWidth,
167
+ screenHeight
168
+ );
148
169
 
149
170
  b64Template = await this.fixImageTemplateScale(b64Template, {
150
- defaultImageTemplateScale, ignoreDefaultImageTemplateScale,
151
- fixImageTemplateScale, ...scale
171
+ defaultImageTemplateScale,
172
+ ignoreDefaultImageTemplateScale,
173
+ fixImageTemplateScale,
174
+ ...scale,
152
175
  });
153
176
 
154
177
  const comparisonOpts = {
@@ -160,18 +183,20 @@ export default class ImageElementFinder {
160
183
  comparisonOpts.method = imageMatchMethod;
161
184
  }
162
185
  if (multiple) {
163
- results.push(...(await compareImages(MATCH_TEMPLATE_MODE,
164
- b64Screenshot,
165
- b64Template,
166
- comparisonOpts)));
186
+ results.push(
187
+ ...(await compareImages(
188
+ MATCH_TEMPLATE_MODE,
189
+ b64Screenshot,
190
+ b64Template,
191
+ comparisonOpts
192
+ ))
193
+ );
167
194
  } else {
168
- results.push(await compareImages(MATCH_TEMPLATE_MODE,
169
- b64Screenshot,
170
- b64Template,
171
- comparisonOpts));
195
+ results.push(
196
+ await compareImages(MATCH_TEMPLATE_MODE, b64Screenshot, b64Template, comparisonOpts)
197
+ );
172
198
  }
173
199
  return true;
174
-
175
200
  } catch (err) {
176
201
  // if compareImages fails, we'll get a specific error, but we should
177
202
  // retry, so trap that and just return false to trigger the next round of
@@ -226,47 +251,42 @@ export default class ImageElementFinder {
226
251
  * Ensure that the image template sent in for a find is of a suitable size
227
252
  *
228
253
  * @param {string} b64Template - base64-encoded image
229
- * @param {int} screenWidth - width of screen
230
- * @param {int} screenHeight - height of screen
254
+ * @param {number} screenWidth - width of screen
255
+ * @param {number} screenHeight - height of screen
231
256
  *
232
- * @returns {string} base64-encoded image, potentially resized
257
+ * @returns {Promise<string>} base64-encoded image, potentially resized
233
258
  */
234
- async ensureTemplateSize (b64Template, screenWidth, screenHeight) {
259
+ async ensureTemplateSize(b64Template, screenWidth, screenHeight) {
235
260
  let imgObj = await imageUtil.getJimpImage(b64Template);
236
261
  let {width: tplWidth, height: tplHeight} = imgObj.bitmap;
237
262
 
238
- log.info(`Template image is ${tplWidth}x${tplHeight}. Screen size is ${screenWidth}x${screenHeight}`);
263
+ log.info(
264
+ `Template image is ${tplWidth}x${tplHeight}. Screen size is ${screenWidth}x${screenHeight}`
265
+ );
239
266
  // if the template fits inside the screen dimensions, we're good
240
267
  if (tplWidth <= screenWidth && tplHeight <= screenHeight) {
241
268
  return b64Template;
242
269
  }
243
270
 
244
- log.info(`Scaling template image from ${tplWidth}x${tplHeight} to match ` +
245
- `screen at ${screenWidth}x${screenHeight}`);
271
+ log.info(
272
+ `Scaling template image from ${tplWidth}x${tplHeight} to match ` +
273
+ `screen at ${screenWidth}x${screenHeight}`
274
+ );
246
275
  // otherwise, scale it to fit inside the screen dimensions
247
276
  imgObj = imgObj.scaleToFit(screenWidth, screenHeight);
248
277
  return (await imgObj.getBuffer(imageUtil.MIME_PNG)).toString('base64');
249
278
  }
250
279
 
251
- /**
252
- * @typedef {Object} Screenshot
253
- * @property {string} b64Screenshot - base64 based screenshot string
254
- */
255
- /**
256
- * @typedef {Object} ScreenshotScale
257
- * @property {float} xScale - Scale ratio for width
258
- * @property {float} yScale - Scale ratio for height
259
- */
260
280
  /**
261
281
  * Get the screenshot image that will be used for find by element, potentially
262
282
  * altering it in various ways based on user-requested settings
263
283
  *
264
- * @param {int} screenWidth - width of screen
265
- * @param {int} screenHeight - height of screen
284
+ * @param {number} screenWidth - width of screen
285
+ * @param {number} screenHeight - height of screen
266
286
  *
267
- * @returns {Screenshot, ?ScreenshotScale} base64-encoded screenshot and ScreenshotScale
287
+ * @returns {Promise<Screenshot & {scale?: ScreenshotScale}>} base64-encoded screenshot and ScreenshotScale
268
288
  */
269
- async getScreenshotForImageFind (screenWidth, screenHeight) {
289
+ async getScreenshotForImageFind(screenWidth, screenHeight) {
270
290
  if (!this.driver.getScreenshot) {
271
291
  throw new Error("This driver does not support the required 'getScreenshot' command");
272
292
  }
@@ -283,8 +303,10 @@ export default class ImageElementFinder {
283
303
  }
284
304
 
285
305
  if (screenWidth < 1 || screenHeight < 1) {
286
- log.warn(`The retrieved screen size ${screenWidth}x${screenHeight} does ` +
287
- `not seem to be valid. No changes will be applied to the screenshot`);
306
+ log.warn(
307
+ `The retrieved screen size ${screenWidth}x${screenHeight} does ` +
308
+ `not seem to be valid. No changes will be applied to the screenshot`
309
+ );
288
310
  return {b64Screenshot};
289
311
  }
290
312
 
@@ -296,8 +318,10 @@ export default class ImageElementFinder {
296
318
  let {width: shotWidth, height: shotHeight} = imgObj.bitmap;
297
319
 
298
320
  if (shotWidth < 1 || shotHeight < 1) {
299
- log.warn(`The retrieved screenshot size ${shotWidth}x${shotHeight} does ` +
300
- `not seem to be valid. No changes will be applied to the screenshot`);
321
+ log.warn(
322
+ `The retrieved screenshot size ${shotWidth}x${shotHeight} does ` +
323
+ `not seem to be valid. No changes will be applied to the screenshot`
324
+ );
301
325
  return {b64Screenshot};
302
326
  }
303
327
 
@@ -319,13 +343,17 @@ export default class ImageElementFinder {
319
343
  const screenAR = screenWidth / screenHeight;
320
344
  const shotAR = shotWidth / shotHeight;
321
345
  if (Math.round(screenAR * FLOAT_PRECISION) === Math.round(shotAR * FLOAT_PRECISION)) {
322
- log.info(`Screenshot aspect ratio '${shotAR}' (${shotWidth}x${shotHeight}) matched ` +
323
- `screen aspect ratio '${screenAR}' (${screenWidth}x${screenHeight})`);
346
+ log.info(
347
+ `Screenshot aspect ratio '${shotAR}' (${shotWidth}x${shotHeight}) matched ` +
348
+ `screen aspect ratio '${screenAR}' (${screenWidth}x${screenHeight})`
349
+ );
324
350
  } else {
325
- log.warn(`When trying to find an element, determined that the screen ` +
326
- `aspect ratio and screenshot aspect ratio are different. Screen ` +
327
- `is ${screenWidth}x${screenHeight} whereas screenshot is ` +
328
- `${shotWidth}x${shotHeight}.`);
351
+ log.warn(
352
+ `When trying to find an element, determined that the screen ` +
353
+ `aspect ratio and screenshot aspect ratio are different. Screen ` +
354
+ `is ${screenWidth}x${screenHeight} whereas screenshot is ` +
355
+ `${shotWidth}x${shotHeight}.`
356
+ );
329
357
 
330
358
  // In the case where the x-scale and y-scale are different, we need to decide
331
359
  // which one to respect, otherwise the screenshot and template will end up
@@ -342,9 +370,11 @@ export default class ImageElementFinder {
342
370
  const yScale = (1.0 * shotHeight) / screenHeight;
343
371
  const scaleFactor = xScale >= yScale ? yScale : xScale;
344
372
 
345
- log.warn(`Resizing screenshot to ${shotWidth * scaleFactor}x${shotHeight * scaleFactor} to match ` +
346
- `screen aspect ratio so that image element coordinates have a ` +
347
- `greater chance of being correct.`);
373
+ log.warn(
374
+ `Resizing screenshot to ${shotWidth * scaleFactor}x${shotHeight * scaleFactor} to match ` +
375
+ `screen aspect ratio so that image element coordinates have a ` +
376
+ `greater chance of being correct.`
377
+ );
348
378
  imgObj = imgObj.resize(shotWidth * scaleFactor, shotHeight * scaleFactor);
349
379
 
350
380
  scale.xScale *= scaleFactor;
@@ -359,8 +389,10 @@ export default class ImageElementFinder {
359
389
  // screenshot size like `@driver.window_rect #=>x=0, y=0, width=1080, height=1794` and
360
390
  // `"deviceScreenSize"=>"1080x1920"`
361
391
  if (screenWidth !== shotWidth && screenHeight !== shotHeight) {
362
- log.info(`Scaling screenshot from ${shotWidth}x${shotHeight} to match ` +
363
- `screen at ${screenWidth}x${screenHeight}`);
392
+ log.info(
393
+ `Scaling screenshot from ${shotWidth}x${shotHeight} to match ` +
394
+ `screen at ${screenWidth}x${screenHeight}`
395
+ );
364
396
  imgObj = imgObj.resize(screenWidth, screenHeight);
365
397
 
366
398
  scale.xScale *= (1.0 * screenWidth) / shotWidth;
@@ -372,14 +404,14 @@ export default class ImageElementFinder {
372
404
  }
373
405
 
374
406
  /**
375
- * @typedef {Object} ImageTemplateSettings
407
+ * @typedef ImageTemplateSettings
376
408
  * @property {boolean} fixImageTemplateScale - fixImageTemplateScale in device-settings
377
- * @property {float} defaultImageTemplateScale - defaultImageTemplateScale in device-settings
409
+ * @property {number} defaultImageTemplateScale - defaultImageTemplateScale in device-settings
378
410
  * @property {boolean} ignoreDefaultImageTemplateScale - Ignore defaultImageTemplateScale if it has true.
379
411
  * If b64Template has been scaled to defaultImageTemplateScale or should ignore the scale,
380
412
  * this parameter should be true. e.g. click in image-element module
381
- * @property {float} xScale - Scale ratio for width
382
- * @property {float} yScale - Scale ratio for height
413
+ * @property {number} xScale - Scale ratio for width
414
+ * @property {number} yScale - Scale ratio for height
383
415
 
384
416
  */
385
417
  /**
@@ -391,9 +423,9 @@ export default class ImageElementFinder {
391
423
  * matched in the screenshot
392
424
  * @param {ImageTemplateSettings} opts - Image template scale related options
393
425
  *
394
- * @returns {string} base64-encoded scaled template screenshot
426
+ * @returns {Promise<string>} base64-encoded scaled template screenshot
395
427
  */
396
- async fixImageTemplateScale (b64Template, opts = {}) {
428
+ async fixImageTemplateScale(b64Template, opts) {
397
429
  if (!opts) {
398
430
  return b64Template;
399
431
  }
@@ -403,7 +435,7 @@ export default class ImageElementFinder {
403
435
  defaultImageTemplateScale = DEFAULT_TEMPLATE_IMAGE_SCALE,
404
436
  ignoreDefaultImageTemplateScale = false,
405
437
  xScale = DEFAULT_FIX_IMAGE_TEMPLATE_SCALE,
406
- yScale = DEFAULT_FIX_IMAGE_TEMPLATE_SCALE
438
+ yScale = DEFAULT_FIX_IMAGE_TEMPLATE_SCALE,
407
439
  } = opts;
408
440
 
409
441
  if (ignoreDefaultImageTemplateScale) {
@@ -424,13 +456,21 @@ export default class ImageElementFinder {
424
456
  }
425
457
 
426
458
  // xScale and yScale can be NaN if defaultImageTemplateScale is string, for example
427
- if (!parseFloat(xScale) || !parseFloat(yScale)) {
459
+ if (!parseFloat(String(xScale)) || !parseFloat(String(yScale))) {
428
460
  return b64Template;
429
461
  }
430
462
 
431
463
  // Return if the scale is default, 1, value
432
- if (Math.round(xScale * FLOAT_PRECISION) === Math.round(DEFAULT_FIX_IMAGE_TEMPLATE_SCALE * FLOAT_PRECISION)
433
- && Math.round(yScale * FLOAT_PRECISION === Math.round(DEFAULT_FIX_IMAGE_TEMPLATE_SCALE * FLOAT_PRECISION))) {
464
+ if (
465
+ Math.round(xScale * FLOAT_PRECISION) ===
466
+ Math.round(DEFAULT_FIX_IMAGE_TEMPLATE_SCALE * FLOAT_PRECISION) &&
467
+ Math.round(
468
+ Number(
469
+ yScale * FLOAT_PRECISION ===
470
+ Math.round(DEFAULT_FIX_IMAGE_TEMPLATE_SCALE * FLOAT_PRECISION)
471
+ )
472
+ )
473
+ ) {
434
474
  return b64Template;
435
475
  }
436
476
 
@@ -439,12 +479,30 @@ export default class ImageElementFinder {
439
479
 
440
480
  const scaledWidth = baseTempWidth * xScale;
441
481
  const scaledHeight = baseTempHeigh * yScale;
442
- log.info(`Scaling template image from ${baseTempWidth}x${baseTempHeigh}` +
443
- ` to ${scaledWidth}x${scaledHeight}`);
482
+ log.info(
483
+ `Scaling template image from ${baseTempWidth}x${baseTempHeigh}` +
484
+ ` to ${scaledWidth}x${scaledHeight}`
485
+ );
444
486
  log.info(`The ratio is ${xScale} and ${yScale}`);
445
487
  imgTempObj = await imgTempObj.resize(scaledWidth, scaledHeight);
446
488
  return (await imgTempObj.getBuffer(imageUtil.MIME_PNG)).toString('base64');
447
489
  }
448
490
  }
449
491
 
450
- export { W3C_ELEMENT_KEY, MJSONWP_ELEMENT_KEY, DEFAULT_SETTINGS, DEFAULT_FIX_IMAGE_TEMPLATE_SCALE };
492
+ export {W3C_ELEMENT_KEY, MJSONWP_ELEMENT_KEY, DEFAULT_SETTINGS, DEFAULT_FIX_IMAGE_TEMPLATE_SCALE};
493
+
494
+ /**
495
+ * @typedef {import('@appium/types').ExternalDriver} ExternalDriver
496
+ * @typedef {import('@appium/types').Element} Element
497
+ */
498
+
499
+ /**
500
+ * @typedef Screenshot
501
+ * @property {string} b64Screenshot - base64 based screenshot string
502
+ */
503
+
504
+ /**
505
+ * @typedef ScreenshotScale
506
+ * @property {number} xScale - Scale ratio for width
507
+ * @property {number} yScale - Scale ratio for height
508
+ */