This repository has been archived by the owner on Oct 25, 2023. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 130
/
Copy pathfind.js
375 lines (322 loc) · 14.2 KB
/
find.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
import log from '../logger';
import { logger, imageUtil } from 'appium-support';
import _ from 'lodash';
import { errors } from '../../..';
import { MATCH_TEMPLATE_MODE } from './images';
import { W3C_ELEMENT_KEY, MJSONWP_ELEMENT_KEY } from '../../protocol/protocol';
import { ImageElement } from '../image-element';
const commands = {}, helpers = {}, extensions = {};
const IMAGE_STRATEGY = '-image';
const CUSTOM_STRATEGY = '-custom';
// Override the following function for your own driver, and the rest is taken
// care of!
// helpers.findElOrEls = async function (strategy, selector, mult, context) {}
// strategy: locator strategy
// selector: the actual selector for finding an element
// mult: multiple elements or just one?
// context: finding an element from the root context? or starting from another element
//
// Returns an object which adheres to the way the JSON Wire Protocol represents elements:
// { ELEMENT: # } eg: { ELEMENT: 3 } or { ELEMENT: 1.023 }
helpers.findElOrElsWithProcessing = async function (strategy, selector, mult, context) {
this.validateLocatorStrategy(strategy);
try {
return await this.findElOrEls(strategy, selector, mult, context);
} catch (err) {
if (this.opts.printPageSourceOnFindFailure) {
const src = await this.getPageSource();
log.debug(`Error finding element${mult ? 's' : ''}: ${err.message}`);
log.debug(`Page source requested through 'printPageSourceOnFindFailure':`);
log.debug(src);
}
// still want the error to occur
throw err;
}
};
commands.findElement = async function (strategy, selector) {
if (strategy === IMAGE_STRATEGY) {
return await this.findByImage(selector, {multiple: false});
} else if (strategy === CUSTOM_STRATEGY) {
return await this.findByCustom(selector, false);
}
return await this.findElOrElsWithProcessing(strategy, selector, false);
};
commands.findElements = async function (strategy, selector) {
if (strategy === IMAGE_STRATEGY) {
return await this.findByImage(selector, {multiple: true});
} else if (strategy === CUSTOM_STRATEGY) {
return await this.findByCustom(selector, true);
}
return await this.findElOrElsWithProcessing(strategy, selector, true);
};
commands.findElementFromElement = async function (strategy, selector, elementId) {
return await this.findElOrElsWithProcessing(strategy, selector, false, elementId);
};
commands.findElementsFromElement = async function (strategy, selector, elementId) {
return await this.findElOrElsWithProcessing(strategy, selector, true, elementId);
};
/**
* Find an element using a custom plugin specified by the customFindModules cap.
*
* @param {string} selector - the selector which the plugin will use to find
* elements
* @param {boolean} multiple - whether we want one element or multiple
*
* @returns {WebElement} - WebDriver element or list of elements
*/
commands.findByCustom = async function (selector, multiple) {
const plugins = this.opts.customFindModules;
// first ensure the user has registered one or more find plugins
if (!plugins) {
// TODO this info should go in docs instead; update when docs for this
// feature exist
throw new Error('Finding an element using a plugin is currently an ' +
'incubating feature. To use it you must manually install one or more ' +
'plugin modules in a way that they can be required by Appium, for ' +
'example installing them from the Appium directory, installing them ' +
'globally, or installing them elsewhere and passing an absolute path as ' +
'the capability. Then construct an object where the key is the shortcut ' +
'name for this plugin and the value is the module name or absolute path, ' +
'for example: {"p1": "my-find-plugin"}, and pass this in as the ' +
"'customFindModules' capability.");
}
// then do some basic checking of the type of the capability
if (!_.isPlainObject(plugins)) {
throw new Error("Invalid format for the 'customFindModules' capability. " +
'It should be an object with keys corresponding to the short names and ' +
'values corresponding to the full names of the element finding plugins');
}
// get the name of the particular plugin used for this invocation of find,
// and separate it from the selector we will pass to the plugin
let [plugin, realSelector] = selector.split(':');
// if the user didn't specify a plugin for this find invocation, and we had
// multiple plugins registered, that's a problem
if (_.size(plugins) > 1 && !realSelector) {
throw new Error(`Multiple element finding plugins were registered ` +
`(${_.keys(plugins)}), but your selector did not indicate which plugin ` +
`to use. Ensure you put the short name of the plugin followed by ':' as ` +
`the initial part of the selector string.`);
}
// but if they did not specify a plugin and we only have one plugin, just use
// that one
if (_.size(plugins) === 1 && !realSelector) {
realSelector = plugin;
plugin = _.keys(plugins)[0];
}
if (!plugins[plugin]) {
throw new Error(`Selector specified use of element finding plugin ` +
`'${plugin}' but it was not registered in the 'customFindModules' ` +
`capability.`);
}
let finder;
try {
log.debug(`Find plugin '${plugin}' requested; will attempt to use it ` +
`from '${plugins[plugin]}'`);
finder = require(plugins[plugin]);
} catch (err) {
throw new Error(`Could not load your custom find module '${plugin}'. Did ` +
`you put it somewhere Appium can 'require' it? Original error: ${err}`);
}
if (!finder || !_.isFunction(finder.find)) {
throw new Error('Your custom find module did not appear to be constructed ' +
'correctly. It needs to export an object with a `find` method.');
}
const customFinderLog = logger.getLogger(plugin);
let elements;
const condition = async () => {
// get a list of matched elements from the custom finder, which can
// potentially use the entire suite of methods the current driver provides.
// the finder should always return a list of elements, but may use the
// knowledge of whether we are looking for one or many to perform internal
// optimizations
elements = await finder.find(this, customFinderLog, realSelector, multiple);
// if we're looking for multiple elements, or if we're looking for only
// one and found it, we're done
if (!_.isEmpty(elements) || multiple) {
return true;
}
// otherwise we should retry, so return false to trigger the retry loop
return false;
};
try {
// make sure we respect implicit wait
await this.implicitWaitForCondition(condition);
} catch (err) {
if (err.message.match(/Condition unmet/)) {
throw new errors.NoSuchElementError();
}
throw err;
}
return multiple ? elements : elements[0];
};
/**
* @typedef {Object} FindByImageOptions
* @property {boolean} [shouldCheckStaleness=false] - whether this call to find an
* image is merely to check staleness. If so we can bypass a lot of logic
* @property {boolean} [multiple=false] - Whether we are finding one element or
* multiple
*/
/**
* Find a screen rect represented by an ImageElement corresponding to an image
* template sent in by the client
*
* @param {string} b64Template - base64-encoded image used as a template to be
* matched in the screenshot
* @param {FindByImageOptions} - additional options
*
* @returns {WebElement} - WebDriver element with a special id prefix
*/
helpers.findByImage = async function (b64Template, {
shouldCheckStaleness = false,
multiple = false,
}) {
const {
imageMatchThreshold: threshold,
fixImageTemplateSize
} = this.settings.getSettings();
log.info(`Finding image element with match threshold ${threshold}`);
if (!this.getWindowSize) {
throw new Error("This driver does not support the required 'getWindowSize' command");
}
const {width: screenWidth, height: screenHeight} = await this.getWindowSize();
// someone might have sent in a template that's larger than the screen
// dimensions. If so let's check and cut it down to size since the algorithm
// will not work unless we do. But because it requires some potentially
// expensive commands, only do this if the user has requested it in settings.
if (fixImageTemplateSize) {
b64Template = await this.ensureTemplateSize(b64Template, screenWidth,
screenHeight);
}
let rect = null;
const condition = async () => {
try {
let b64Screenshot = await this.getScreenshotForImageFind(screenWidth, screenHeight);
rect = (await this.compareImages(MATCH_TEMPLATE_MODE, b64Screenshot,
b64Template, {threshold})).rect;
return true;
} catch (err) {
// if compareImages fails, we'll get a specific error, but we should
// retry, so trap that and just return false to trigger the next round of
// implicitly waiting. For other errors, throw them to get out of the
// implicit wait loop
if (err.message.match(/Cannot find any occurrences/)) {
return false;
}
throw err;
}
};
try {
await this.implicitWaitForCondition(condition);
} catch (err) {
// this `implicitWaitForCondition` method will throw a 'Condition unmet'
// error if an element is not found eventually. In that case, we will
// handle the element not found response below. In the case where get some
// _other_ kind of error, it means something blew up totally apart from the
// implicit wait timeout. We should not mask that error and instead throw
// it straightaway
if (!err.message.match(/Condition unmet/)) {
throw err;
}
}
if (!rect) {
if (multiple) {
return [];
}
throw new errors.NoSuchElementError();
}
log.info(`Image template matched: ${JSON.stringify(rect)}`);
const imgEl = new ImageElement(b64Template, rect);
// if we're just checking staleness, return straightaway so we don't add
// a new element to the cache. shouldCheckStaleness does not support multiple
// elements, since it is a purely internal mechanism
if (shouldCheckStaleness) {
return imgEl;
}
this._imgElCache.set(imgEl.id, imgEl);
const protoKey = this.isW3CProtocol() ? W3C_ELEMENT_KEY : MJSONWP_ELEMENT_KEY;
const protocolEl = imgEl.asElement(protoKey);
return multiple ? [protocolEl] : protocolEl;
};
/**
* Ensure that the image template sent in for a find is of a suitable size
*
* @param {string} b64Template - base64-encoded image
* @param {int} screenWidth - width of screen
* @param {int} screenHeight - height of screen
*
* @returns {string} base64-encoded image, potentially resized
*/
helpers.ensureTemplateSize = async function (b64Template, screenWidth, screenHeight) {
let imgObj = await imageUtil.getJimpImage(b64Template);
let {width: tplWidth, height: tplHeight} = imgObj.bitmap;
// if the template fits inside the screen dimensions, we're good
if (tplWidth <= screenWidth && tplHeight <= screenHeight) {
return b64Template;
}
// otherwise, scale it to fit inside the screen dimensions
imgObj = imgObj.scaleToFit(screenWidth, screenHeight);
return (await imgObj.getBuffer(imageUtil.MIME_PNG)).toString('base64');
};
/**
* Get the screenshot image that will be used for find by element, potentially
* altering it in various ways based on user-requested settings
*
* @param {int} screenWidth - width of screen
* @param {int} screenHeight - height of screen
*
* @returns {string} base64-encoded screenshot
*/
helpers.getScreenshotForImageFind = async function (screenWidth, screenHeight) {
if (!this.getScreenshot) {
throw new Error("This driver does not support the required 'getScreenshot' command");
}
let b64Screenshot = await this.getScreenshot();
// if the user has requested not to correct for aspect or size differences
// between the screenshot and the screen, just return the screenshot now
if (!this.settings.getSettings().fixImageFindScreenshotDims) {
log.info(`Not verifying screenshot dimensions match screen`);
return b64Screenshot;
}
// otherwise, do some verification on the screenshot to make sure it matches
// the screen size and aspect ratio
log.info('Verifying screenshot size and aspect ratio');
let imgObj = await imageUtil.getJimpImage(b64Screenshot);
let {width: shotWidth, height: shotHeight} = imgObj.bitmap;
if (screenWidth === shotWidth && screenHeight === shotHeight) {
// the height and width of the screenshot and the device screen match, which
// means we should be safe when doing template matches
log.info('Screenshot size matched screen size');
return b64Screenshot;
}
// otherwise, if they don't match, it could spell problems for the accuracy
// of coordinates returned by the image match algorithm, since we match based
// on the screenshot coordinates not the device coordinates themselves. There
// are two potential types of mismatch: aspect ratio mismatch and scale
// mismatch. we need to detect and fix both
const screenAR = screenWidth / screenHeight;
const shotAR = shotWidth / shotHeight;
if (screenAR === shotAR) {
log.info('Screenshot aspect ratio matched screen aspect ratio');
} else {
log.warn(`When trying to find an element, determined that the screen ` +
`aspect ratio and screenshot aspect ratio are different. Screen ` +
`is ${screenWidth}x${screenHeight} whereas screenshot is ` +
`${shotWidth}x${shotHeight}.`);
shotWidth = shotWidth / (shotAR / screenAR);
log.warn(`Resizing screenshot to ${shotWidth}x${shotHeight} to match ` +
`screen aspect ratio so that image element coordinates have a ` +
`greater chance of being correct.`);
imgObj = imgObj.resize(shotWidth, shotHeight);
}
// now we know the aspect ratios match, but there might still be a scale
// mismatch, so just resize based on the screen dimensions
if (screenWidth !== shotWidth) {
log.info(`Scaling screenshot from ${shotWidth}x${shotHeight} to match ` +
`screen at ${screenWidth}x${screenHeight}`);
imgObj = imgObj.resize(screenWidth, screenHeight);
}
return (await imgObj.getBuffer(imageUtil.MIME_PNG)).toString('base64');
};
Object.assign(extensions, commands, helpers);
export { commands, helpers, IMAGE_STRATEGY, CUSTOM_STRATEGY };
export default extensions;