mirror of
https://github.com/appium/appium.git
synced 2026-11-11 12:54:55 +08:00
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1d3994699 | ||
|
|
58fbe803c7 | ||
|
|
6d1fbe7149 | ||
|
|
3d09b0f1e9 | ||
|
|
2cc3baf735 | ||
|
|
b35d22a7d8 | ||
|
|
9b874c7c06 | ||
|
|
dc2e932559 | ||
|
|
1a58a88001 | ||
|
|
3ade20ec31 | ||
|
|
de7e41bbf7 | ||
|
|
b65c3a8286 | ||
|
|
c750e6e1fe | ||
|
|
cfb657b29e | ||
|
|
0f570e3b5d | ||
|
|
5071c652e5 | ||
|
|
a3221c08ad | ||
|
|
ffbc8b4229 | ||
|
|
537c29e5d6 | ||
|
|
63041b99bb | ||
|
|
c5e6eb194e | ||
|
|
a04ceca921 | ||
|
|
71555a0227 |
@@ -8,10 +8,10 @@ pool:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node_10_x:
|
||||
node_version: 10.x
|
||||
node_12_x:
|
||||
node_version: 12.x
|
||||
node_14_x:
|
||||
node_version: 14.x
|
||||
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
appium-base-driver
|
||||
===================
|
||||
# appium-base-driver
|
||||
|
||||
> **NOTE: `appium-base-driver` for Appium 2.x has moved to the [`2.0` branch of `appium/appium`](https://github.com/appium/appium/tree/2.0/packages/base-driver)**
|
||||
|
||||
[](https://npmjs.org/package/appium-base-driver)
|
||||
[](https://npmjs.org/package/appium-base-driver)
|
||||
[](https://david-dm.org/appium/appium-base-driver)
|
||||
|
||||
@@ -55,8 +55,8 @@ export { statusCodes, getSummaryByCode };
|
||||
|
||||
// W3C capabilities parser
|
||||
import * as caps from './lib/basedriver/capabilities';
|
||||
const { processCapabilities, isStandardCap } = caps;
|
||||
export { processCapabilities, isStandardCap };
|
||||
const { processCapabilities, isStandardCap, validateCaps } = caps;
|
||||
export { processCapabilities, isStandardCap, validateCaps };
|
||||
|
||||
// Web socket helpers
|
||||
import * as ws from './lib/express/websocket';
|
||||
|
||||
@@ -208,6 +208,7 @@ helpers.findByImage = async function findByImage (b64Template, {
|
||||
}) {
|
||||
const {
|
||||
imageMatchThreshold: threshold,
|
||||
imageMatchMethod,
|
||||
fixImageTemplateSize,
|
||||
fixImageTemplateScale,
|
||||
defaultImageTemplateScale,
|
||||
@@ -229,9 +230,7 @@ helpers.findByImage = async function findByImage (b64Template, {
|
||||
screenHeight);
|
||||
}
|
||||
|
||||
let rect = null;
|
||||
let b64Matched = null;
|
||||
let score = 0;
|
||||
const results = [];
|
||||
const condition = async () => {
|
||||
try {
|
||||
const {b64Screenshot, scale} = await this.getScreenshotForImageFind(screenWidth, screenHeight);
|
||||
@@ -241,12 +240,27 @@ helpers.findByImage = async function findByImage (b64Template, {
|
||||
fixImageTemplateScale, ...scale
|
||||
});
|
||||
|
||||
const comparedImage = await this.compareImages(MATCH_TEMPLATE_MODE, b64Screenshot, b64Template, {threshold, visualize});
|
||||
rect = comparedImage.rect;
|
||||
b64Matched = comparedImage.visualization;
|
||||
score = comparedImage.score;
|
||||
|
||||
const comparisonOpts = {
|
||||
threshold,
|
||||
visualize,
|
||||
multiple,
|
||||
};
|
||||
if (imageMatchMethod) {
|
||||
comparisonOpts.method = imageMatchMethod;
|
||||
}
|
||||
if (multiple) {
|
||||
results.push(...(await this.compareImages(MATCH_TEMPLATE_MODE,
|
||||
b64Screenshot,
|
||||
b64Template,
|
||||
comparisonOpts)));
|
||||
} else {
|
||||
results.push(await this.compareImages(MATCH_TEMPLATE_MODE,
|
||||
b64Screenshot,
|
||||
b64Template,
|
||||
comparisonOpts));
|
||||
}
|
||||
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
|
||||
@@ -273,29 +287,28 @@ helpers.findByImage = async function findByImage (b64Template, {
|
||||
}
|
||||
}
|
||||
|
||||
if (!rect) {
|
||||
if (_.isEmpty(results)) {
|
||||
if (multiple) {
|
||||
return [];
|
||||
}
|
||||
throw new errors.NoSuchElementError();
|
||||
}
|
||||
|
||||
log.info(`Image template matched: ${JSON.stringify(rect)}`);
|
||||
if (b64Matched) {
|
||||
log.info(`Matched base64 data: ${b64Matched.substring(0, 200)}...`);
|
||||
}
|
||||
const imgEl = new ImageElement(b64Template, rect, score, b64Matched);
|
||||
const elements = results.map(({rect, score, visualization}) => {
|
||||
log.info(`Image template matched: ${JSON.stringify(rect)}`);
|
||||
return new ImageElement(b64Template, rect, score, visualization);
|
||||
});
|
||||
|
||||
// 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;
|
||||
return elements[0];
|
||||
}
|
||||
|
||||
const protocolEl = this.registerImageElement(imgEl);
|
||||
const registeredElements = elements.map((imgEl) => this.registerImageElement(imgEl));
|
||||
|
||||
return multiple ? [protocolEl] : protocolEl;
|
||||
return multiple ? registeredElements : registeredElements[0];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,7 +22,21 @@ const DEFAULT_MATCH_THRESHOLD = 0.4;
|
||||
* @param {string} secondImage - Base64-encoded image file.
|
||||
* All image formats, that OpenCV library itself accepts, are supported.
|
||||
* @param {?Object} options [{}] - The content of this dictionary depends
|
||||
* on the actual `mode` value. See the documentation on `appium-support`
|
||||
* on the actual `mode` value.
|
||||
* For MATCH_TEMPLATE_MODE:
|
||||
* - visualize: include the visualization of the match in the result
|
||||
* (default: false)
|
||||
* - threshold: the image match threshold, higher values
|
||||
* require a closer image similarity to match
|
||||
* (default: 0.5)
|
||||
* - multiple: return multiple matches in the image
|
||||
* (default: false)
|
||||
* - matchNeighbourThreshold: if multiple is specified,
|
||||
* determine the number of pixels within
|
||||
* which to consider a pixel as part of the
|
||||
* same match result
|
||||
* (default: 10)
|
||||
* See the documentation in the `appium-support`
|
||||
* module for more details.
|
||||
* @returns {Object} The content of the resulting dictionary depends
|
||||
* on the actual `mode` and `options` values. See the documentation on
|
||||
@@ -34,7 +48,8 @@ const DEFAULT_MATCH_THRESHOLD = 0.4;
|
||||
commands.compareImages = async function compareImages (mode, firstImage, secondImage, options = {}) {
|
||||
const img1 = Buffer.from(firstImage, 'base64');
|
||||
const img2 = Buffer.from(secondImage, 'base64');
|
||||
let result = {};
|
||||
let result = null;
|
||||
|
||||
switch (_.toLower(mode)) {
|
||||
case MATCH_FEATURES_MODE.toLowerCase():
|
||||
result = await imageUtil.getImagesMatches(img1, img2, options);
|
||||
@@ -45,17 +60,34 @@ commands.compareImages = async function compareImages (mode, firstImage, secondI
|
||||
case MATCH_TEMPLATE_MODE.toLowerCase():
|
||||
// firstImage/img1 is the full image and secondImage/img2 is the partial one
|
||||
result = await imageUtil.getImageOccurrence(img1, img2, options);
|
||||
|
||||
if (options.multiple) {
|
||||
return result.multiple.map(convertVisualizationToBase64);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new errors.InvalidArgumentError(`'${mode}' images comparison mode is unknown. ` +
|
||||
`Only ${JSON.stringify([MATCH_FEATURES_MODE, GET_SIMILARITY_MODE, MATCH_TEMPLATE_MODE])} modes are supported.`);
|
||||
}
|
||||
if (!_.isEmpty(result.visualization)) {
|
||||
result.visualization = result.visualization.toString('base64');
|
||||
}
|
||||
return result;
|
||||
|
||||
return convertVisualizationToBase64(result);
|
||||
};
|
||||
|
||||
/**
|
||||
* base64 encodes the visualization part of the result
|
||||
* (if necessary)
|
||||
*
|
||||
* @param {OccurenceResult} element - occurrence result
|
||||
*
|
||||
**/
|
||||
function convertVisualizationToBase64 (element) {
|
||||
if (!_.isEmpty(element.visualization)) {
|
||||
element.visualization = element.visualization.toString('base64');
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
Object.assign(extensions, commands, helpers);
|
||||
export { commands, helpers, DEFAULT_MATCH_THRESHOLD, MATCH_TEMPLATE_MODE };
|
||||
export default extensions;
|
||||
|
||||
@@ -8,6 +8,12 @@ const GLOBAL_DEFAULT_SETTINGS = {
|
||||
// element will not be found
|
||||
imageMatchThreshold: DEFAULT_MATCH_THRESHOLD,
|
||||
|
||||
// One of possible image matching methods.
|
||||
// Read https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_imgproc/py_template_matching/py_template_matching.html
|
||||
// for more details.
|
||||
// TM_CCOEFF_NORMED by default
|
||||
imageMatchMethod: '',
|
||||
|
||||
// if the image returned by getScreenshot differs in size or aspect ratio
|
||||
// from the screen, attempt to fix it automatically
|
||||
fixImageFindScreenshotDims: true,
|
||||
@@ -54,6 +60,7 @@ const GLOBAL_DEFAULT_SETTINGS = {
|
||||
// drivers can choose to ignore them for their own settings validation, etc...
|
||||
const BASEDRIVER_HANDLED_SETTINGS = [
|
||||
'imageMatchThreshold',
|
||||
'imageMatchMethod',
|
||||
'fixImageFindScreenshotDims',
|
||||
'fixImageTemplateSize',
|
||||
'fixImageTemplateScale',
|
||||
|
||||
@@ -13,8 +13,9 @@ import { desiredCapabilityConstraints } from './desired-caps';
|
||||
import { validateCaps } from './capabilities';
|
||||
import B from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
import { util } from 'appium-support';
|
||||
import { ImageElement, makeImageElementCache, getImgElFromArgs } from './image-element';
|
||||
import {
|
||||
ImageElement, makeImageElementCache, getImgElFromArgs
|
||||
} from './image-element';
|
||||
import AsyncLock from 'async-lock';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
@@ -428,34 +429,6 @@ class BaseDriver extends Protocol {
|
||||
this.clearNewCommandTimeout();
|
||||
}
|
||||
|
||||
async getSwipeOptions (gestures, touchCount = 1) {
|
||||
let startX = this.helpers.getCoordDefault(gestures[0].options.x),
|
||||
startY = this.helpers.getCoordDefault(gestures[0].options.y),
|
||||
endX = this.helpers.getCoordDefault(gestures[2].options.x),
|
||||
endY = this.helpers.getCoordDefault(gestures[2].options.y),
|
||||
duration = this.helpers.getSwipeTouchDuration(gestures[1]),
|
||||
element = gestures[0].options.element,
|
||||
destElement = gestures[2].options.element || gestures[0].options.element;
|
||||
|
||||
// there's no destination element handling in bootstrap and since it applies to all platforms, we handle it here
|
||||
if (util.hasValue(destElement)) {
|
||||
let locResult = await this.getLocationInView(destElement);
|
||||
let sizeResult = await this.getSize(destElement);
|
||||
let offsetX = (Math.abs(endX) < 1 && Math.abs(endX) > 0) ? sizeResult.width * endX : endX;
|
||||
let offsetY = (Math.abs(endY) < 1 && Math.abs(endY) > 0) ? sizeResult.height * endY : endY;
|
||||
endX = locResult.x + offsetX;
|
||||
endY = locResult.y + offsetY;
|
||||
// if the target element was provided, the coordinates for the destination need to be relative to it.
|
||||
if (util.hasValue(element)) {
|
||||
let firstElLocation = await this.getLocationInView(element);
|
||||
endX -= firstElLocation.x;
|
||||
endY -= firstElLocation.y;
|
||||
}
|
||||
}
|
||||
// clients are responsible to use these options correctly
|
||||
return {startX, startY, endX, endY, duration, touchCount, element};
|
||||
}
|
||||
|
||||
proxyActive (/* sessionId */) {
|
||||
return false;
|
||||
}
|
||||
|
||||
+11
-26
@@ -2,12 +2,11 @@ import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
import logger from './logger';
|
||||
import { tempDir, fs, util, zip, net, timing } from 'appium-support';
|
||||
import { system, tempDir, fs, util, zip, net, timing } from 'appium-support';
|
||||
import LRU from 'lru-cache';
|
||||
import AsyncLock from 'async-lock';
|
||||
import axios from 'axios';
|
||||
|
||||
|
||||
const IPA_EXT = '.ipa';
|
||||
const ZIP_EXTS = ['.zip', IPA_EXT];
|
||||
const ZIP_MIME_TYPES = [
|
||||
@@ -323,7 +322,15 @@ async function unzipApp (zipPath, dstRoot, supportedAppExtensions) {
|
||||
try {
|
||||
logger.debug(`Unzipping '${zipPath}'`);
|
||||
const timer = new timing.Timer().start();
|
||||
const extractionOpts = {};
|
||||
/**
|
||||
* Attempt to use use the system `unzip` (e.g., `/usr/bin/unzip`) due
|
||||
* to the significant performance improvement it provides over the native
|
||||
* JS "unzip" implementation.
|
||||
* @type {import('appium-support/lib/zip').ExtractAllOptions}
|
||||
*/
|
||||
const extractionOpts = {
|
||||
useSystemUnzip: !system.isWindows(),
|
||||
};
|
||||
// https://github.com/appium/appium/issues/14100
|
||||
if (path.extname(zipPath) === IPA_EXT) {
|
||||
logger.debug(`Enforcing UTF-8 encoding on the extracted file names for '${path.basename(zipPath)}'`);
|
||||
@@ -359,28 +366,6 @@ function isPackageOrBundle (app) {
|
||||
return (/^([a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+)+$/).test(app);
|
||||
}
|
||||
|
||||
function getCoordDefault (val) {
|
||||
// going the long way and checking for undefined and null since
|
||||
// we can't be assured `elId` is a string and not an int. Same
|
||||
// thing with destElement below.
|
||||
return util.hasValue(val) ? val : 0.5;
|
||||
}
|
||||
|
||||
function getSwipeTouchDuration (waitGesture) {
|
||||
// the touch action api uses ms, we want seconds
|
||||
// 0.8 is the default time for the operation
|
||||
let duration = 0.8;
|
||||
if (typeof waitGesture.options.ms !== 'undefined' && waitGesture.options.ms) {
|
||||
duration = waitGesture.options.ms / 1000;
|
||||
if (duration === 0) {
|
||||
// set to a very low number, since they wanted it fast
|
||||
// but below 0.1 becomes 0 steps, which causes errors
|
||||
duration = 0.1;
|
||||
}
|
||||
}
|
||||
return duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all instances 'firstKey' and create a duplicate with the key 'secondKey',
|
||||
* Do the same thing in reverse. If we find 'secondKey', create a duplicate with the key 'firstKey'.
|
||||
@@ -443,5 +428,5 @@ function parseCapsArray (cap) {
|
||||
}
|
||||
|
||||
export {
|
||||
configureApp, isPackageOrBundle, getCoordDefault, getSwipeTouchDuration, duplicateKeys, parseCapsArray
|
||||
configureApp, isPackageOrBundle, duplicateKeys, parseCapsArray
|
||||
};
|
||||
|
||||
@@ -41,7 +41,6 @@ class JWProxy {
|
||||
keepAlive: opts.keepAlive ?? true,
|
||||
maxSockets: 10,
|
||||
maxFreeSockets: 5,
|
||||
timeout: 60000,
|
||||
};
|
||||
this.httpAgent = new http.Agent(agentOpts);
|
||||
this.httpsAgent = new https.Agent(agentOpts);
|
||||
|
||||
@@ -280,6 +280,7 @@ const METHOD_MAP = {
|
||||
},
|
||||
'/session/:sessionId/actions': {
|
||||
POST: {command: 'performActions', payloadParams: {required: ['actions']}},
|
||||
DELETE: {command: 'releaseActions'},
|
||||
},
|
||||
'/session/:sessionId/touch/longclick': {
|
||||
POST: {command: 'touchLongClick', payloadParams: {required: ['elements']}}
|
||||
|
||||
+6
-6
@@ -4,7 +4,7 @@
|
||||
"keywords": [
|
||||
"appium"
|
||||
],
|
||||
"version": "7.4.1",
|
||||
"version": "7.9.0",
|
||||
"author": "appium",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
@@ -35,10 +35,10 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"appium-support": "^2.48.0",
|
||||
"appium-support": "^2.54.0",
|
||||
"async-lock": "^1.0.0",
|
||||
"asyncbox": "^2.3.1",
|
||||
"axios": "^0.21.0",
|
||||
"axios": "^0.x",
|
||||
"bluebird": "^3.5.3",
|
||||
"body-parser": "^1.18.2",
|
||||
"colors": "^1.1.2",
|
||||
@@ -50,7 +50,7 @@
|
||||
"method-override": "^3.0.0",
|
||||
"morgan": "^1.9.0",
|
||||
"serve-favicon": "^2.4.5",
|
||||
"source-map-support": "^0.5.5",
|
||||
"source-map-support": "^0.x",
|
||||
"validate.js": "^0.13.0",
|
||||
"webdriverio": "^6.0.2",
|
||||
"ws": "^7.0.0"
|
||||
@@ -81,10 +81,10 @@
|
||||
"eslint-config-appium": "^4.0.1",
|
||||
"finalhandler": "^1.1.1",
|
||||
"gulp": "^4.0.0",
|
||||
"mocha": "^8.0.1",
|
||||
"mocha": "^9.0.0",
|
||||
"querystring": "^0.2.0",
|
||||
"pre-commit": "^1.1.3",
|
||||
"serve-static": "^1.13.1",
|
||||
"sinon": "^9.0.0"
|
||||
"sinon": "^11.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,9 @@ describe('finding elements by image', function () {
|
||||
});
|
||||
it('should find image elements happypath', async function () {
|
||||
const d = new TestDriver();
|
||||
basicStub(d);
|
||||
const {compareStub} = basicStub(d);
|
||||
compareStub.returns([{rect, score}]);
|
||||
|
||||
const els = await d.findByImage(template, {multiple: true});
|
||||
els.should.have.length(1);
|
||||
basicImgElVerify(els[0], d);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { isPackageOrBundle, duplicateKeys, parseCapsArray } from '../../lib/basedriver/helpers';
|
||||
import { zip, fs, tempDir } from 'appium-support';
|
||||
import { configureApp, isPackageOrBundle, duplicateKeys, parseCapsArray } from '../../lib/basedriver/helpers';
|
||||
import chai from 'chai';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
|
||||
import sinon from 'sinon';
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
const should = chai.should();
|
||||
@@ -92,6 +93,31 @@ describe('helpers', function () {
|
||||
duplicateKeys(input, 'foo', 'FOO').should.deep.equal(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#configureApp', function () {
|
||||
let sandbox;
|
||||
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.createSandbox();
|
||||
sandbox.stub(zip, 'extractAllTo').resolves();
|
||||
sandbox.stub(zip, 'assertValidZip').resolves();
|
||||
sandbox.stub(fs, 'mv').resolves();
|
||||
sandbox.stub(fs, 'exists').resolves(true);
|
||||
sandbox.stub(fs, 'hash').resolves('0xDEADBEEF');
|
||||
sandbox.stub(fs, 'glob').resolves(['/path/to/an.apk']);
|
||||
sandbox.stub(fs, 'rimraf').resolves();
|
||||
sandbox.stub(tempDir, 'openDir').resolves('/some/dir');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('should pass "useSystemUnzip" flag through to appium-support', async function () {
|
||||
await configureApp('/path/to/an.apk.zip', '.apk');
|
||||
zip.extractAllTo.getCall(0).lastArg.useSystemUnzip.should.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseCapsArray', function () {
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('Protocol', function () {
|
||||
}
|
||||
let hash = shasum.digest('hex').substring(0, 8);
|
||||
// Modify the hash whenever the protocol has intentionally been modified.
|
||||
hash.should.equal('52aacae0');
|
||||
hash.should.equal('b1dff09e');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user