Best JavaScript code snippet using appium-base-driver
find.js
Source:find.js
1import log from '../logger';2import { logger, imageUtil } from 'appium-support';3import _ from 'lodash';4import { errors } from '../../..';5import { MATCH_TEMPLATE_MODE } from './images';6import { ImageElement, DEFAULT_TEMPLATE_IMAGE_SCALE } from '../image-element';7const commands = {}, helpers = {}, extensions = {};8const IMAGE_STRATEGY = '-image';9const CUSTOM_STRATEGY = '-custom';10// Used to compare ratio and screen width11// Pixel is basically under 1080 for example. 100K is probably enough fo a while.12const FLOAT_PRECISION = 100000;13// Override the following function for your own driver, and the rest is taken14// care of!15// helpers.findElOrEls = async function (strategy, selector, mult, context) {}16// strategy: locator strategy17// selector: the actual selector for finding an element18// mult: multiple elements or just one?19// context: finding an element from the root context? or starting from another element20//21// Returns an object which adheres to the way the JSON Wire Protocol represents elements:22// { ELEMENT: # } eg: { ELEMENT: 3 } or { ELEMENT: 1.023 }23helpers.findElOrElsWithProcessing = async function findElOrElsWithProcessing (strategy, selector, mult, context) {24 this.validateLocatorStrategy(strategy);25 try {26 return await this.findElOrEls(strategy, selector, mult, context);27 } catch (err) {28 if (this.opts.printPageSourceOnFindFailure) {29 const src = await this.getPageSource();30 log.debug(`Error finding element${mult ? 's' : ''}: ${err.message}`);31 log.debug(`Page source requested through 'printPageSourceOnFindFailure':`);32 log.debug(src);33 }34 // still want the error to occur35 throw err;36 }37};38commands.findElement = async function findElement (strategy, selector) {39 if (strategy === IMAGE_STRATEGY) {40 return await this.findByImage(selector, {multiple: false});41 } else if (strategy === CUSTOM_STRATEGY) {42 return await this.findByCustom(selector, false);43 }44 return await this.findElOrElsWithProcessing(strategy, selector, false);45};46commands.findElements = async function findElements (strategy, selector) {47 if (strategy === IMAGE_STRATEGY) {48 return await this.findByImage(selector, {multiple: true});49 } else if (strategy === CUSTOM_STRATEGY) {50 return await this.findByCustom(selector, true);51 }52 return await this.findElOrElsWithProcessing(strategy, selector, true);53};54commands.findElementFromElement = async function findElementFromElement (strategy, selector, elementId) {55 return await this.findElOrElsWithProcessing(strategy, selector, false, elementId);56};57commands.findElementsFromElement = async function findElementsFromElement (strategy, selector, elementId) {58 return await this.findElOrElsWithProcessing(strategy, selector, true, elementId);59};60/**61 * Find an element using a custom plugin specified by the customFindModules cap.62 *63 * @param {string} selector - the selector which the plugin will use to find64 * elements65 * @param {boolean} multiple - whether we want one element or multiple66 *67 * @returns {WebElement} - WebDriver element or list of elements68 */69commands.findByCustom = async function findByCustom (selector, multiple) {70 const plugins = this.opts.customFindModules;71 // first ensure the user has registered one or more find plugins72 if (!plugins) {73 // TODO this info should go in docs instead; update when docs for this74 // feature exist75 throw new Error('Finding an element using a plugin is currently an ' +76 'incubating feature. To use it you must manually install one or more ' +77 'plugin modules in a way that they can be required by Appium, for ' +78 'example installing them from the Appium directory, installing them ' +79 'globally, or installing them elsewhere and passing an absolute path as ' +80 'the capability. Then construct an object where the key is the shortcut ' +81 'name for this plugin and the value is the module name or absolute path, ' +82 'for example: {"p1": "my-find-plugin"}, and pass this in as the ' +83 "'customFindModules' capability.");84 }85 // then do some basic checking of the type of the capability86 if (!_.isPlainObject(plugins)) {87 throw new Error("Invalid format for the 'customFindModules' capability. " +88 'It should be an object with keys corresponding to the short names and ' +89 'values corresponding to the full names of the element finding plugins');90 }91 // get the name of the particular plugin used for this invocation of find,92 // and separate it from the selector we will pass to the plugin93 let [plugin, realSelector] = selector.split(':');94 // if the user didn't specify a plugin for this find invocation, and we had95 // multiple plugins registered, that's a problem96 if (_.size(plugins) > 1 && !realSelector) {97 throw new Error(`Multiple element finding plugins were registered ` +98 `(${_.keys(plugins)}), but your selector did not indicate which plugin ` +99 `to use. Ensure you put the short name of the plugin followed by ':' as ` +100 `the initial part of the selector string.`);101 }102 // but if they did not specify a plugin and we only have one plugin, just use103 // that one104 if (_.size(plugins) === 1 && !realSelector) {105 realSelector = plugin;106 plugin = _.keys(plugins)[0];107 }108 if (!plugins[plugin]) {109 throw new Error(`Selector specified use of element finding plugin ` +110 `'${plugin}' but it was not registered in the 'customFindModules' ` +111 `capability.`);112 }113 let finder;114 try {115 log.debug(`Find plugin '${plugin}' requested; will attempt to use it ` +116 `from '${plugins[plugin]}'`);117 finder = require(plugins[plugin]);118 } catch (err) {119 throw new Error(`Could not load your custom find module '${plugin}'. Did ` +120 `you put it somewhere Appium can 'require' it? Original error: ${err}`);121 }122 if (!finder || !_.isFunction(finder.find)) {123 throw new Error('Your custom find module did not appear to be constructed ' +124 'correctly. It needs to export an object with a `find` method.');125 }126 const customFinderLog = logger.getLogger(plugin);127 let elements;128 const condition = async () => {129 // get a list of matched elements from the custom finder, which can130 // potentially use the entire suite of methods the current driver provides.131 // the finder should always return a list of elements, but may use the132 // knowledge of whether we are looking for one or many to perform internal133 // optimizations134 elements = await finder.find(this, customFinderLog, realSelector, multiple);135 // if we're looking for multiple elements, or if we're looking for only136 // one and found it, we're done137 if (!_.isEmpty(elements) || multiple) {138 return true;139 }140 // otherwise we should retry, so return false to trigger the retry loop141 return false;142 };143 try {144 // make sure we respect implicit wait145 await this.implicitWaitForCondition(condition);146 } catch (err) {147 if (err.message.match(/Condition unmet/)) {148 throw new errors.NoSuchElementError();149 }150 throw err;151 }152 return multiple ? elements : elements[0];153};154/**155 * @typedef {Object} FindByImageOptions156 * @property {boolean} [shouldCheckStaleness=false] - whether this call to find an157 * image is merely to check staleness. If so we can bypass a lot of logic158 * @property {boolean} [multiple=false] - Whether we are finding one element or159 * multiple160 * @property {boolean} [ignoreDefaultImageTemplateScale=false] - Whether we161 * ignore defaultImageTemplateScale. It can be used when you would like to162 * scale b64Template with defaultImageTemplateScale setting.163 */164/**165 * Find a screen rect represented by an ImageElement corresponding to an image166 * template sent in by the client167 *168 * @param {string} b64Template - base64-encoded image used as a template to be169 * matched in the screenshot170 * @param {FindByImageOptions} - additional options171 *172 * @returns {WebElement} - WebDriver element with a special id prefix173 */174helpers.findByImage = async function findByImage (b64Template, {175 shouldCheckStaleness = false,176 multiple = false,177 ignoreDefaultImageTemplateScale = false,178}) {179 const {180 imageMatchThreshold: threshold,181 imageMatchMethod,182 fixImageTemplateSize,183 fixImageTemplateScale,184 defaultImageTemplateScale,185 getMatchedImageResult: visualize186 } = this.settings.getSettings();187 log.info(`Finding image element with match threshold ${threshold}`);188 if (!this.getWindowSize) {189 throw new Error("This driver does not support the required 'getWindowSize' command");190 }191 const {width: screenWidth, height: screenHeight} = await this.getWindowSize();192 // someone might have sent in a template that's larger than the screen193 // dimensions. If so let's check and cut it down to size since the algorithm194 // will not work unless we do. But because it requires some potentially195 // expensive commands, only do this if the user has requested it in settings.196 if (fixImageTemplateSize) {197 b64Template = await this.ensureTemplateSize(b64Template, screenWidth,198 screenHeight);199 }200 const results = [];201 const condition = async () => {202 try {203 const {b64Screenshot, scale} = await this.getScreenshotForImageFind(screenWidth, screenHeight);204 b64Template = await this.fixImageTemplateScale(b64Template, {205 defaultImageTemplateScale, ignoreDefaultImageTemplateScale,206 fixImageTemplateScale, ...scale207 });208 const comparisonOpts = {209 threshold,210 visualize,211 multiple,212 };213 if (imageMatchMethod) {214 comparisonOpts.method = imageMatchMethod;215 }216 if (multiple) {217 results.push(...(await this.compareImages(MATCH_TEMPLATE_MODE,218 b64Screenshot,219 b64Template,220 comparisonOpts)));221 } else {222 results.push(await this.compareImages(MATCH_TEMPLATE_MODE,223 b64Screenshot,224 b64Template,225 comparisonOpts));226 }227 return true;228 } catch (err) {229 // if compareImages fails, we'll get a specific error, but we should230 // retry, so trap that and just return false to trigger the next round of231 // implicitly waiting. For other errors, throw them to get out of the232 // implicit wait loop233 if (err.message.match(/Cannot find any occurrences/)) {234 return false;235 }236 throw err;237 }238 };239 try {240 await this.implicitWaitForCondition(condition);241 } catch (err) {242 // this `implicitWaitForCondition` method will throw a 'Condition unmet'243 // error if an element is not found eventually. In that case, we will244 // handle the element not found response below. In the case where get some245 // _other_ kind of error, it means something blew up totally apart from the246 // implicit wait timeout. We should not mask that error and instead throw247 // it straightaway248 if (!err.message.match(/Condition unmet/)) {249 throw err;250 }251 }252 if (_.isEmpty(results)) {253 if (multiple) {254 return [];255 }256 throw new errors.NoSuchElementError();257 }258 const elements = results.map(({rect, score, visualization}) => {259 log.info(`Image template matched: ${JSON.stringify(rect)}`);260 return new ImageElement(b64Template, rect, score, visualization);261 });262 // if we're just checking staleness, return straightaway so we don't add263 // a new element to the cache. shouldCheckStaleness does not support multiple264 // elements, since it is a purely internal mechanism265 if (shouldCheckStaleness) {266 return elements[0];267 }268 const registeredElements = elements.map((imgEl) => this.registerImageElement(imgEl));269 return multiple ? registeredElements : registeredElements[0];270};271/**272 * Ensure that the image template sent in for a find is of a suitable size273 *274 * @param {string} b64Template - base64-encoded image275 * @param {int} screenWidth - width of screen276 * @param {int} screenHeight - height of screen277 *278 * @returns {string} base64-encoded image, potentially resized279 */280helpers.ensureTemplateSize = async function ensureTemplateSize (b64Template, screenWidth, screenHeight) {281 let imgObj = await imageUtil.getJimpImage(b64Template);282 let {width: tplWidth, height: tplHeight} = imgObj.bitmap;283 log.info(`Template image is ${tplWidth}x${tplHeight}. Screen size is ${screenWidth}x${screenHeight}`);284 // if the template fits inside the screen dimensions, we're good285 if (tplWidth <= screenWidth && tplHeight <= screenHeight) {286 return b64Template;287 }288 log.info(`Scaling template image from ${tplWidth}x${tplHeight} to match ` +289 `screen at ${screenWidth}x${screenHeight}`);290 // otherwise, scale it to fit inside the screen dimensions291 imgObj = imgObj.scaleToFit(screenWidth, screenHeight);292 return (await imgObj.getBuffer(imageUtil.MIME_PNG)).toString('base64');293};294/**295 * @typedef {Object} Screenshot296 * @property {string} b64Screenshot - base64 based screenshot string297 */298/**299 * @typedef {Object} ScreenshotScale300 * @property {float} xScale - Scale ratio for width301 * @property {float} yScale - Scale ratio for height302 */303/**304 * Get the screenshot image that will be used for find by element, potentially305 * altering it in various ways based on user-requested settings306 *307 * @param {int} screenWidth - width of screen308 * @param {int} screenHeight - height of screen309 *310 * @returns {Screenshot, ?ScreenshotScale} base64-encoded screenshot and ScreenshotScale311 */312helpers.getScreenshotForImageFind = async function getScreenshotForImageFind (screenWidth, screenHeight) {313 if (!this.getScreenshot) {314 throw new Error("This driver does not support the required 'getScreenshot' command");315 }316 let b64Screenshot = await this.getScreenshot();317 // if the user has requested not to correct for aspect or size differences318 // between the screenshot and the screen, just return the screenshot now319 if (!this.settings.getSettings().fixImageFindScreenshotDims) {320 log.info(`Not verifying screenshot dimensions match screen`);321 return {b64Screenshot};322 }323 if (screenWidth < 1 || screenHeight < 1) {324 log.warn(`The retrieved screen size ${screenWidth}x${screenHeight} does ` +325 `not seem to be valid. No changes will be applied to the screenshot`);326 return {b64Screenshot};327 }328 // otherwise, do some verification on the screenshot to make sure it matches329 // the screen size and aspect ratio330 log.info('Verifying screenshot size and aspect ratio');331 let imgObj = await imageUtil.getJimpImage(b64Screenshot);332 let {width: shotWidth, height: shotHeight} = imgObj.bitmap;333 if (shotWidth < 1 || shotHeight < 1) {334 log.warn(`The retrieved screenshot size ${shotWidth}x${shotHeight} does ` +335 `not seem to be valid. No changes will be applied to the screenshot`);336 return {b64Screenshot};337 }338 if (screenWidth === shotWidth && screenHeight === shotHeight) {339 // the height and width of the screenshot and the device screen match, which340 // means we should be safe when doing template matches341 log.info('Screenshot size matched screen size');342 return {b64Screenshot};343 }344 // otherwise, if they don't match, it could spell problems for the accuracy345 // of coordinates returned by the image match algorithm, since we match based346 // on the screenshot coordinates not the device coordinates themselves. There347 // are two potential types of mismatch: aspect ratio mismatch and scale348 // mismatch. We need to detect and fix both349 const scale = {xScale: 1.0, yScale: 1.0};350 const screenAR = screenWidth / screenHeight;351 const shotAR = shotWidth / shotHeight;352 if (Math.round(screenAR * FLOAT_PRECISION) === Math.round(shotAR * FLOAT_PRECISION)) {353 log.info(`Screenshot aspect ratio '${shotAR}' (${shotWidth}x${shotHeight}) matched ` +354 `screen aspect ratio '${screenAR}' (${screenWidth}x${screenHeight})`);355 } else {356 log.warn(`When trying to find an element, determined that the screen ` +357 `aspect ratio and screenshot aspect ratio are different. Screen ` +358 `is ${screenWidth}x${screenHeight} whereas screenshot is ` +359 `${shotWidth}x${shotHeight}.`);360 // In the case where the x-scale and y-scale are different, we need to decide361 // which one to respect, otherwise the screenshot and template will end up362 // being resized in a way that changes its aspect ratio (distorts it). For example, let's say:363 // this.getScreenshot(shotWidth, shotHeight) is 540x397,364 // this.getDeviceSize(screenWidth, screenHeight) is 1080x1920.365 // The ratio would then be {xScale: 0.5, yScale: 0.2}.366 // In this case, we must should `yScale: 0.2` as scaleFactor, because367 // if we select the xScale, the height will be bigger than real screenshot size368 // which is used to image comparison by OpenCV as a base image.369 // All of this is primarily useful when the screenshot is a horizontal slice taken out of the370 // screen (for example not including top/bottom nav bars)371 const xScale = (1.0 * shotWidth) / screenWidth;372 const yScale = (1.0 * shotHeight) / screenHeight;373 const scaleFactor = xScale >= yScale ? yScale : xScale;374 log.warn(`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 imgObj = imgObj.resize(shotWidth * scaleFactor, shotHeight * scaleFactor);378 scale.xScale *= scaleFactor;379 scale.yScale *= scaleFactor;380 shotWidth = imgObj.bitmap.width;381 shotHeight = imgObj.bitmap.height;382 }383 // Resize based on the screen dimensions only if both width and height are mismatched384 // since except for that, it might be a situation which is different window rect and385 // screenshot size like `@driver.window_rect #=>x=0, y=0, width=1080, height=1794` and386 // `"deviceScreenSize"=>"1080x1920"`387 if (screenWidth !== shotWidth && screenHeight !== shotHeight) {388 log.info(`Scaling screenshot from ${shotWidth}x${shotHeight} to match ` +389 `screen at ${screenWidth}x${screenHeight}`);390 imgObj = imgObj.resize(screenWidth, screenHeight);391 scale.xScale *= (1.0 * screenWidth) / shotWidth;392 scale.yScale *= (1.0 * screenHeight) / shotHeight;393 }394 b64Screenshot = (await imgObj.getBuffer(imageUtil.MIME_PNG)).toString('base64');395 return {b64Screenshot, scale};396};397/**398 * @typedef {Object} ImageTemplateSettings399 * @property {boolean} fixImageTemplateScale - fixImageTemplateScale in device-settings400 * @property {float} defaultImageTemplateScale - defaultImageTemplateScale in device-settings401 * @property {boolean} ignoreDefaultImageTemplateScale - Ignore defaultImageTemplateScale if it has true.402 * If b64Template has been scaled to defaultImageTemplateScale or should ignore the scale,403 * this parameter should be true. e.g. click in image-element module404 * @property {float} xScale - Scale ratio for width405 * @property {float} yScale - Scale ratio for height406 */407/**408 * Get a image that will be used for template maching.409 * Returns scaled image if scale ratio is provided.410 *411 *412 * @param {string} b64Template - base64-encoded image used as a template to be413 * matched in the screenshot414 * @param {ImageTemplateSettings} opts - Image template scale related options415 *416 * @returns {string} base64-encoded scaled template screenshot417 */418const DEFAULT_FIX_IMAGE_TEMPLATE_SCALE = 1;419helpers.fixImageTemplateScale = async function fixImageTemplateScale (b64Template, opts = {}) {420 if (!opts) {421 return b64Template;422 }423 let {424 fixImageTemplateScale = false,425 defaultImageTemplateScale = DEFAULT_TEMPLATE_IMAGE_SCALE,426 ignoreDefaultImageTemplateScale = false,427 xScale = DEFAULT_FIX_IMAGE_TEMPLATE_SCALE,428 yScale = DEFAULT_FIX_IMAGE_TEMPLATE_SCALE429 } = opts;430 if (ignoreDefaultImageTemplateScale) {431 defaultImageTemplateScale = DEFAULT_TEMPLATE_IMAGE_SCALE;432 }433 // Default434 if (defaultImageTemplateScale === DEFAULT_TEMPLATE_IMAGE_SCALE && !fixImageTemplateScale) {435 return b64Template;436 }437 // Calculate xScale and yScale Appium should scale438 if (fixImageTemplateScale) {439 xScale *= defaultImageTemplateScale;440 yScale *= defaultImageTemplateScale;441 } else {442 xScale = yScale = 1 * defaultImageTemplateScale;443 }444 // xScale and yScale can be NaN if defaultImageTemplateScale is string, for example445 if (!parseFloat(xScale) || !parseFloat(yScale)) {446 return b64Template;447 }448 // Return if the scale is default, 1, value449 if (Math.round(xScale * FLOAT_PRECISION) === Math.round(DEFAULT_FIX_IMAGE_TEMPLATE_SCALE * FLOAT_PRECISION)450 && Math.round(yScale * FLOAT_PRECISION === Math.round(DEFAULT_FIX_IMAGE_TEMPLATE_SCALE * FLOAT_PRECISION))) {451 return b64Template;452 }453 let imgTempObj = await imageUtil.getJimpImage(b64Template);454 let {width: baseTempWidth, height: baseTempHeigh} = imgTempObj.bitmap;455 const scaledWidth = baseTempWidth * xScale;456 const scaledHeight = baseTempHeigh * yScale;457 log.info(`Scaling template image from ${baseTempWidth}x${baseTempHeigh}` +458 ` to ${scaledWidth}x${scaledHeight}`);459 log.info(`The ratio is ${xScale} and ${yScale}`);460 imgTempObj = await imgTempObj.resize(scaledWidth, scaledHeight);461 return (await imgTempObj.getBuffer(imageUtil.MIME_PNG)).toString('base64');462};463Object.assign(extensions, commands, helpers);464export { commands, helpers, IMAGE_STRATEGY, CUSTOM_STRATEGY };...
Using AI Code Generation
1const { BaseDriver } = require('appium-base-driver');2const { fixImageTemplateScale } = require('./helpers');3const driver = new BaseDriver();4describe('Test fixImageTemplateScale method', function () {5 it('should return the correct scale', function () {6 const scale = fixImageTemplateScale(driver, 2);7 });8});
Using AI Code Generation
1const helpers = require('appium-base-driver').androidHelpers;2const scale = helpers.fixImageTemplateScale('path_to_image');3console.log(scale);4const helpers = require('appium-base-driver').androidHelpers;5const scale = helpers.getDevicePixelRatio();6console.log(scale);7const helpers = require('appium-base-driver').androidHelpers;8const scale = helpers.getDisplayDensity();9console.log(scale);10const helpers = require('appium-base-driver').androidHelpers;11const scale = helpers.getDisplayRotation();12console.log(scale);13const helpers = require('appium-base-driver').androidHelpers;14const scale = helpers.getDisplaySize();15console.log(scale);16const helpers = require('appium-base-driver').androidHelpers;17const scale = helpers.getIMEList();18console.log(scale);19const helpers = require('appium-base-driver').androidHelpers;20const scale = helpers.getIMEActivated();21console.log(scale);22const helpers = require('appium-base-driver').androidHelpers;23const scale = helpers.getDeviceTime();24console.log(scale);25const helpers = require('appium-base-driver').androidHelpers;26const scale = helpers.getDeviceSysLanguage();27console.log(scale);28const helpers = require('appium-base-driver').androidHelpers;29const scale = helpers.getDeviceCountry();30console.log(scale);31const helpers = require('appium-base-driver').androidHelpers;32const scale = helpers.getDeviceTimezone();33console.log(scale);
Using AI Code Generation
1var helpers = require('appium-base-driver').androidHelpers;2var desired = {3};4var driver = new wd.Builder().withCapabilities(desired).build();5driver.init(desired).then(function() {6 return driver.sleep(5000);7}).then(function() {8}).then(function() {9}).then(function() {10}).then(function() {11}).then(function() {12}).then(function() {13}).then(function() {14}).then(function() {15}).then(function() {16}).then(function() {17}).then(function() {18}).then(function() {19}).then(function() {20}).then(function() {21}).then(function() {22}).then(function() {
Using AI Code Generation
1const { helpers } = require('appium-base-driver');2helpers.fixImageTemplateScale({scale: 2});3const { helpers } = require('appium-base-driver');4module.exports = {helpers};5{6"dependencies": {7 }8}9const { helpers } = require('appium-base-driver');10helpers.fixImageTemplateScale({scale: 2});11const { helpers } = require('appium-base-driver');12module.exports = {helpers};13{14"dependencies": {15 }16}
Using AI Code Generation
1const helpers = require('appium-base-driver').AndroidHelpers;2const path = require('path');3const image = path.resolve('/Users/isaacmurchie/Downloads/Screen Shot 2016-09-22 at 1.36.57 PM.png');4const image2 = path.resolve('/Users/isaacmurchie/Downloads/Screen Shot 2016-09-22 at 1.36.57 PM.png');5const image3 = helpers.fixImageTemplateScale(image);6const image4 = helpers.fixImageTemplateScale(image2);7console.log(image3, image4);8const helpers = require('appium-base-driver').AndroidHelpers;9const path = require('path');10const image = path.resolve('/Users/isaacmurchie/Downloads/Screen Shot 2016-09-22 at 1.36.57 PM.png');11const image2 = path.resolve('/Users/isaacmurchie/Downloads/Screen Shot 2016-09-22 at 1.36.57 PM.png');12const image3 = helpers.fixImageTemplateScale(image);13const image4 = helpers.fixImageTemplateScale(image2);14console.log(image3, image4);
Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.
You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.
Get 100 minutes of automation test minutes FREE!!