mirror of
https://github.com/appium/appium.git
synced 2026-10-31 22:32:24 +08:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c745352c65 | ||
|
|
1d89e2a5f8 | ||
|
|
2dfae495ff |
Generated
+8
-8
@@ -19438,12 +19438,12 @@
|
||||
}
|
||||
},
|
||||
"packages/appium": {
|
||||
"version": "3.2.1",
|
||||
"version": "3.2.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@appium/base-driver": "^10.2.1",
|
||||
"@appium/base-plugin": "^3.1.0",
|
||||
"@appium/base-driver": "^10.2.2",
|
||||
"@appium/base-plugin": "^3.1.1",
|
||||
"@appium/docutils": "^2.2.2",
|
||||
"@appium/logger": "^2.0.5",
|
||||
"@appium/schema": "^1.1.0",
|
||||
@@ -19527,7 +19527,7 @@
|
||||
},
|
||||
"packages/base-driver": {
|
||||
"name": "@appium/base-driver",
|
||||
"version": "10.2.1",
|
||||
"version": "10.2.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@appium/support": "^7.0.6",
|
||||
@@ -19583,10 +19583,10 @@
|
||||
},
|
||||
"packages/base-plugin": {
|
||||
"name": "@appium/base-plugin",
|
||||
"version": "3.1.0",
|
||||
"version": "3.1.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@appium/base-driver": "^10.2.1",
|
||||
"@appium/base-driver": "^10.2.2",
|
||||
"@appium/support": "^7.0.6",
|
||||
"@appium/types": "^1.2.1"
|
||||
},
|
||||
@@ -19961,10 +19961,10 @@
|
||||
},
|
||||
"packages/fake-plugin": {
|
||||
"name": "@appium/fake-plugin",
|
||||
"version": "4.1.0",
|
||||
"version": "4.1.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@appium/base-plugin": "^3.1.0",
|
||||
"@appium/base-plugin": "^3.1.1",
|
||||
"@appium/support": "^7.0.6",
|
||||
"asyncbox": "6.1.0",
|
||||
"lodash": "4.17.23"
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.2.2](https://github.com/appium/appium/compare/appium@3.2.1...appium@3.2.2) (2026-03-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **appium:** Exclude package.json from tsconfig ([#22052](https://github.com/appium/appium/issues/22052)) ([1d89e2a](https://github.com/appium/appium/commit/1d89e2a5f8e478f3331666d3c308427ab834471a))
|
||||
|
||||
|
||||
|
||||
## [3.2.1](https://github.com/appium/appium/compare/appium@3.2.0...appium@3.2.1) (2026-03-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "appium",
|
||||
"version": "3.2.1",
|
||||
"version": "3.2.2",
|
||||
"description": "Automation for Apps.",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -32,16 +32,15 @@
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"build",
|
||||
"build/lib",
|
||||
"build/types",
|
||||
"index.js",
|
||||
"driver.*",
|
||||
"support.*",
|
||||
"plugin.*",
|
||||
"scripts/autoinstall-extensions.js",
|
||||
"types",
|
||||
"tsconfig.json",
|
||||
"!build/tsconfig.tsbuildinfo",
|
||||
"!build/test"
|
||||
"tsconfig.json"
|
||||
],
|
||||
"scripts": {
|
||||
"build:docs": "node docs/scripts/build-docs.js",
|
||||
@@ -61,8 +60,8 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.ts\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-driver": "^10.2.1",
|
||||
"@appium/base-plugin": "^3.1.0",
|
||||
"@appium/base-driver": "^10.2.2",
|
||||
"@appium/base-plugin": "^3.1.1",
|
||||
"@appium/docutils": "^2.2.2",
|
||||
"@appium/logger": "^2.0.5",
|
||||
"@appium/schema": "^1.1.0",
|
||||
|
||||
@@ -2,7 +2,7 @@ import {DRIVER_TYPE} from '../../../lib/constants';
|
||||
import chai from 'chai';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
import path from 'node:path';
|
||||
import {version as APPIUM_VER} from '../../../package.json';
|
||||
import {APPIUM_VER} from '../../../lib/config';
|
||||
import {FAKE_DRIVER_DIR, PROJECT_ROOT, rewiremock} from '../../helpers';
|
||||
import {initMocks} from './mocks';
|
||||
import {resolveEsmEntryPoint} from '../../../lib/extension/extension-config';
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {SinonSandbox} from 'sinon';
|
||||
import {DRIVER_TYPE, PLUGIN_TYPE} from '../../../lib/constants';
|
||||
import {resolveFixture, rewiremock} from '../../helpers';
|
||||
import {initMocks} from './mocks';
|
||||
import {version as APPIUM_VER} from '../../../package.json';
|
||||
import {APPIUM_VER} from '../../../lib/config';
|
||||
import EventEmitter from 'node:events';
|
||||
import type {MockAppiumSupport, MockGlob, MockPackageChanged} from './mocks';
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"checkJs": true,
|
||||
"types": ["mocha", "chai", "chai-as-promised", "node"]
|
||||
},
|
||||
"include": ["lib", "types", "test", "package.json"],
|
||||
"include": ["lib", "types", "test"],
|
||||
"references": [
|
||||
{"path": "../schema"},
|
||||
{"path": "../types"},
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [10.2.2](https://github.com/appium/appium/compare/@appium/base-driver@10.2.1...@appium/base-driver@10.2.2) (2026-03-09)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-driver
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [10.2.1](https://github.com/appium/appium/compare/@appium/base-driver@10.2.0...@appium/base-driver@10.2.1) (2026-03-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,317 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import {logger, util} from '@appium/support';
|
||||
import {duplicateKeys} from '../basedriver/helpers';
|
||||
import {MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY, PROTOCOLS} from '../constants';
|
||||
|
||||
export const COMMAND_URLS_CONFLICTS = [
|
||||
{
|
||||
commandNames: ['execute', 'executeAsync'],
|
||||
jsonwpConverter: (url) =>
|
||||
url.replace(/\/execute.*/, url.includes('async') ? '/execute_async' : '/execute'),
|
||||
w3cConverter: (url) =>
|
||||
url.replace(/\/execute.*/, url.includes('async') ? '/execute/async' : '/execute/sync'),
|
||||
},
|
||||
{
|
||||
commandNames: ['getElementScreenshot'],
|
||||
jsonwpConverter: (url) => url.replace(/\/element\/([^/]+)\/screenshot$/, '/screenshot/$1'),
|
||||
w3cConverter: (url) => url.replace(/\/screenshot\/([^/]+)/, '/element/$1/screenshot'),
|
||||
},
|
||||
{
|
||||
commandNames: ['getWindowHandles', 'getWindowHandle'],
|
||||
jsonwpConverter(url) {
|
||||
return url.endsWith('/window')
|
||||
? url.replace(/\/window$/, '/window_handle')
|
||||
: url.replace(/\/window\/handle(s?)$/, '/window_handle$1');
|
||||
},
|
||||
w3cConverter(url) {
|
||||
return url.endsWith('/window_handle')
|
||||
? url.replace(/\/window_handle$/, '/window')
|
||||
: url.replace(/\/window_handles$/, '/window/handles');
|
||||
},
|
||||
},
|
||||
{
|
||||
commandNames: ['getProperty'],
|
||||
jsonwpConverter: (w3cUrl) => {
|
||||
const w3cPropertyRegex = /\/element\/([^/]+)\/property\/([^/]+)/;
|
||||
const jsonwpUrl = w3cUrl.replace(w3cPropertyRegex, '/element/$1/attribute/$2');
|
||||
return jsonwpUrl;
|
||||
},
|
||||
w3cConverter: (jsonwpUrl) => jsonwpUrl, // Don't convert JSONWP URL to W3C. W3C accepts /attribute and /property
|
||||
},
|
||||
];
|
||||
const {MJSONWP, W3C} = PROTOCOLS;
|
||||
const DEFAULT_LOG = logger.getLogger('Protocol Converter');
|
||||
|
||||
class ProtocolConverter {
|
||||
/**
|
||||
*
|
||||
* @param {ProxyFunction} proxyFunc
|
||||
* @param {import('@appium/types').AppiumLogger | null} [log=null]
|
||||
*/
|
||||
constructor(proxyFunc, log = null) {
|
||||
this.proxyFunc = proxyFunc;
|
||||
this._downstreamProtocol = null;
|
||||
this._log = log;
|
||||
}
|
||||
|
||||
get log() {
|
||||
return this._log ?? DEFAULT_LOG;
|
||||
}
|
||||
|
||||
set downstreamProtocol(value) {
|
||||
this._downstreamProtocol = value;
|
||||
}
|
||||
|
||||
get downstreamProtocol() {
|
||||
return this._downstreamProtocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* W3C /timeouts can take as many as 3 timeout types at once, MJSONWP /timeouts only takes one
|
||||
* at a time. So if we're using W3C and proxying to MJSONWP and there's more than one timeout type
|
||||
* provided in the request, we need to do 3 proxies and combine the result
|
||||
*
|
||||
* @param {Object} body Request body
|
||||
* @return {Object[]} Array of W3C + MJSONWP compatible timeout objects
|
||||
*/
|
||||
getTimeoutRequestObjects(body) {
|
||||
if (this.downstreamProtocol === W3C && _.has(body, 'ms') && _.has(body, 'type')) {
|
||||
const typeToW3C = (x) => (x === 'page load' ? 'pageLoad' : x);
|
||||
return [
|
||||
{
|
||||
[typeToW3C(body.type)]: body.ms,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (this.downstreamProtocol === MJSONWP && (!_.has(body, 'ms') || !_.has(body, 'type'))) {
|
||||
const typeToJSONWP = (x) => (x === 'pageLoad' ? 'page load' : x);
|
||||
return (
|
||||
_.toPairs(body)
|
||||
// Only transform the entry if ms value is a valid positive float number
|
||||
.filter((pair) => /^\d+(?:[.,]\d*?)?$/.test(`${pair[1]}`))
|
||||
.map(function (pair) {
|
||||
return {
|
||||
type: typeToJSONWP(pair[0]),
|
||||
ms: pair[1],
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return [body];
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy an array of timeout objects and merge the result
|
||||
* @param {string} url Endpoint url
|
||||
* @param {string} method Endpoint method
|
||||
* @param {import('@appium/types').HTTPBody} body Request body
|
||||
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
|
||||
*/
|
||||
async proxySetTimeouts(url, method, body) {
|
||||
let response, resBody;
|
||||
|
||||
const timeoutRequestObjects = this.getTimeoutRequestObjects(body);
|
||||
this.log.debug(
|
||||
`Will send the following request bodies to /timeouts: ${JSON.stringify(
|
||||
timeoutRequestObjects
|
||||
)}`
|
||||
);
|
||||
for (const timeoutObj of timeoutRequestObjects) {
|
||||
[response, resBody] = await this.proxyFunc(url, method, timeoutObj);
|
||||
|
||||
// If we got a non-MJSONWP response, return the result, nothing left to do
|
||||
if (this.downstreamProtocol !== MJSONWP) {
|
||||
return [response, resBody];
|
||||
}
|
||||
|
||||
// If we got an error, return the error right away
|
||||
if (response.statusCode >= 400) {
|
||||
return [response, resBody];
|
||||
}
|
||||
|
||||
// ...Otherwise, continue to the next timeouts call
|
||||
}
|
||||
return [/** @type {import('@appium/types').ProxyResponse} */(response), resBody];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} method
|
||||
* @param {import('@appium/types').HTTPBody} body
|
||||
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
|
||||
*/
|
||||
async proxySetWindow(url, method, body) {
|
||||
const bodyObj = util.safeJsonParse(body);
|
||||
if (_.isPlainObject(bodyObj)) {
|
||||
if (this.downstreamProtocol === W3C && _.has(bodyObj, 'name') && !_.has(bodyObj, 'handle')) {
|
||||
this.log.debug(
|
||||
`Copied 'name' value '${/** @type {import('@appium/types').StringRecord} */ (bodyObj).name}' to 'handle' as per W3C spec`
|
||||
);
|
||||
return await this.proxyFunc(url, method, {
|
||||
.../** @type {import('@appium/types').StringRecord} */ (bodyObj),
|
||||
handle: /** @type {import('@appium/types').StringRecord} */ (bodyObj).name,
|
||||
});
|
||||
}
|
||||
if (
|
||||
this.downstreamProtocol === MJSONWP &&
|
||||
_.has(bodyObj, 'handle') &&
|
||||
!_.has(bodyObj, 'name')
|
||||
) {
|
||||
this.log.debug(
|
||||
`Copied 'handle' value '${/** @type {import('@appium/types').StringRecord} */ (bodyObj).handle}' to 'name' as per JSONWP spec`
|
||||
);
|
||||
return await this.proxyFunc(url, method, {
|
||||
.../** @type {import('@appium/types').StringRecord} */ (bodyObj),
|
||||
name: /** @type {import('@appium/types').StringRecord} */ (bodyObj).handle,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await this.proxyFunc(url, method, body);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} method
|
||||
* @param {import('@appium/types').HTTPBody} body
|
||||
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
|
||||
*/
|
||||
async proxySetValue(url, method, body) {
|
||||
const bodyObj = util.safeJsonParse(body);
|
||||
if (_.isPlainObject(bodyObj) && (util.hasValue(bodyObj.text) || util.hasValue(bodyObj.value))) {
|
||||
let {text, value} = bodyObj;
|
||||
if (util.hasValue(text) && !util.hasValue(value)) {
|
||||
value = _.isString(text) ? [...text] : _.isArray(text) ? text : [];
|
||||
this.log.debug(`Added 'value' property to 'setValue' request body`);
|
||||
} else if (!util.hasValue(text) && util.hasValue(value)) {
|
||||
text = _.isArray(value) ? value.join('') : _.isString(value) ? value : '';
|
||||
this.log.debug(`Added 'text' property to 'setValue' request body`);
|
||||
}
|
||||
return await this.proxyFunc(
|
||||
url,
|
||||
method,
|
||||
{
|
||||
...bodyObj,
|
||||
text,
|
||||
value,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return await this.proxyFunc(url, method, body);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} method
|
||||
* @param {import('@appium/types').HTTPBody} body
|
||||
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
|
||||
*/
|
||||
async proxySetFrame(url, method, body) {
|
||||
const bodyObj = util.safeJsonParse(body);
|
||||
return _.has(bodyObj, 'id') && _.isPlainObject(bodyObj.id)
|
||||
? await this.proxyFunc(url, method, {
|
||||
...bodyObj,
|
||||
id: duplicateKeys(bodyObj.id, MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY),
|
||||
})
|
||||
: await this.proxyFunc(url, method, body);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} method
|
||||
* @param {import('@appium/types').HTTPBody} body
|
||||
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
|
||||
*/
|
||||
async proxyPerformActions(url, method, body) {
|
||||
const bodyObj = util.safeJsonParse(body);
|
||||
return _.isPlainObject(bodyObj)
|
||||
? await this.proxyFunc(
|
||||
url,
|
||||
method,
|
||||
duplicateKeys(bodyObj, MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY)
|
||||
)
|
||||
: await this.proxyFunc(url, method, body);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} method
|
||||
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
|
||||
*/
|
||||
async proxyReleaseActions(url, method) {
|
||||
return await this.proxyFunc(url, method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle "crossing" endpoints for the case
|
||||
* when upstream and downstream drivers operate different protocols
|
||||
*
|
||||
* @param {string} commandName
|
||||
* @param {string} url
|
||||
* @param {string} method
|
||||
* @param {import('@appium/types').HTTPBody} [body]
|
||||
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
|
||||
*/
|
||||
async convertAndProxy(commandName, url, method, body) {
|
||||
if (!this.downstreamProtocol) {
|
||||
return await this.proxyFunc(url, method, body);
|
||||
}
|
||||
|
||||
// Same url, but different arguments
|
||||
switch (commandName) {
|
||||
case 'timeouts':
|
||||
return await this.proxySetTimeouts(url, method, body);
|
||||
case 'setWindow':
|
||||
return await this.proxySetWindow(url, method, body);
|
||||
case 'setValue':
|
||||
return await this.proxySetValue(url, method, body);
|
||||
case 'performActions':
|
||||
return await this.proxyPerformActions(url, method, body);
|
||||
case 'releaseActions':
|
||||
return await this.proxyReleaseActions(url, method);
|
||||
case 'setFrame':
|
||||
return await this.proxySetFrame(url, method, body);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Same arguments, but different URLs
|
||||
for (const {commandNames, jsonwpConverter, w3cConverter} of COMMAND_URLS_CONFLICTS) {
|
||||
if (!commandNames.includes(commandName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rewrittenUrl =
|
||||
this.downstreamProtocol === MJSONWP ? jsonwpConverter(url) : w3cConverter(url);
|
||||
if (rewrittenUrl === url) {
|
||||
this.log.debug(
|
||||
`Did not know how to rewrite the original URL '${url}' ` +
|
||||
`for ${this.downstreamProtocol} protocol`
|
||||
);
|
||||
break;
|
||||
}
|
||||
this.log.info(
|
||||
`Rewrote the original URL '${url}' to '${rewrittenUrl}' ` +
|
||||
`for ${this.downstreamProtocol} protocol`
|
||||
);
|
||||
return await this.proxyFunc(rewrittenUrl, method, body);
|
||||
}
|
||||
|
||||
// No matches found. Proceed normally
|
||||
return await this.proxyFunc(url, method, body);
|
||||
}
|
||||
}
|
||||
|
||||
export default ProtocolConverter;
|
||||
|
||||
/**
|
||||
* @typedef {(url: string, method: string, body?: import('@appium/types').HTTPBody) => Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>} ProxyFunction
|
||||
*/
|
||||
@@ -0,0 +1,284 @@
|
||||
import type {AppiumLogger, HTTPBody, ProxyResponse} from '@appium/types';
|
||||
import _ from 'lodash';
|
||||
import {logger, util} from '@appium/support';
|
||||
import {duplicateKeys} from '../basedriver/helpers';
|
||||
import {MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY, PROTOCOLS} from '../constants';
|
||||
|
||||
export type ProxyFunction = (
|
||||
url: string,
|
||||
method: string,
|
||||
body?: HTTPBody
|
||||
) => Promise<[ProxyResponse, HTTPBody]>;
|
||||
|
||||
export const COMMAND_URLS_CONFLICTS = [
|
||||
{
|
||||
commandNames: ['execute', 'executeAsync'],
|
||||
jsonwpConverter: (url: string) =>
|
||||
url.replace(/\/execute.*/, url.includes('async') ? '/execute_async' : '/execute'),
|
||||
w3cConverter: (url: string) =>
|
||||
url.replace(/\/execute.*/, url.includes('async') ? '/execute/async' : '/execute/sync'),
|
||||
},
|
||||
{
|
||||
commandNames: ['getElementScreenshot'],
|
||||
jsonwpConverter: (url: string) => url.replace(/\/element\/([^/]+)\/screenshot$/, '/screenshot/$1'),
|
||||
w3cConverter: (url: string) => url.replace(/\/screenshot\/([^/]+)/, '/element/$1/screenshot'),
|
||||
},
|
||||
{
|
||||
commandNames: ['getWindowHandles', 'getWindowHandle'],
|
||||
jsonwpConverter(url: string) {
|
||||
return url.endsWith('/window')
|
||||
? url.replace(/\/window$/, '/window_handle')
|
||||
: url.replace(/\/window\/handle(s?)$/, '/window_handle$1');
|
||||
},
|
||||
w3cConverter(url: string) {
|
||||
return url.endsWith('/window_handle')
|
||||
? url.replace(/\/window_handle$/, '/window')
|
||||
: url.replace(/\/window_handles$/, '/window/handles');
|
||||
},
|
||||
},
|
||||
{
|
||||
commandNames: ['getProperty'],
|
||||
jsonwpConverter: (w3cUrl: string) => {
|
||||
const w3cPropertyRegex = /\/element\/([^/]+)\/property\/([^/]+)/;
|
||||
return w3cUrl.replace(w3cPropertyRegex, '/element/$1/attribute/$2');
|
||||
},
|
||||
// Don't convert JSONWP URL to W3C. W3C accepts /attribute and /property
|
||||
w3cConverter: (jsonwpUrl: string) => jsonwpUrl,
|
||||
},
|
||||
] as const;
|
||||
|
||||
const {MJSONWP, W3C} = PROTOCOLS;
|
||||
const DEFAULT_LOG = logger.getLogger('Protocol Converter');
|
||||
|
||||
export class ProtocolConverter {
|
||||
private _downstreamProtocol: string | null | undefined = null;
|
||||
private readonly _log: AppiumLogger | null;
|
||||
|
||||
/**
|
||||
* @param proxyFunc - Function to perform the actual proxy request
|
||||
* @param log - Logger instance, or null to use the default
|
||||
*/
|
||||
constructor(
|
||||
public proxyFunc: ProxyFunction,
|
||||
log: AppiumLogger | null = null
|
||||
) {
|
||||
this._log = log;
|
||||
}
|
||||
|
||||
get log(): AppiumLogger {
|
||||
return this._log ?? DEFAULT_LOG;
|
||||
}
|
||||
|
||||
set downstreamProtocol(value: string | null | undefined) {
|
||||
this._downstreamProtocol = value;
|
||||
}
|
||||
|
||||
get downstreamProtocol(): string | null | undefined {
|
||||
return this._downstreamProtocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle "crossing" endpoints for the case when upstream and downstream
|
||||
* drivers operate different protocols.
|
||||
*/
|
||||
async convertAndProxy(
|
||||
commandName: string,
|
||||
url: string,
|
||||
method: string,
|
||||
body?: HTTPBody
|
||||
): Promise<[ProxyResponse, HTTPBody]> {
|
||||
if (!this.downstreamProtocol) {
|
||||
return await this.proxyFunc(url, method, body);
|
||||
}
|
||||
|
||||
// Same url, but different arguments
|
||||
switch (commandName) {
|
||||
case 'timeouts':
|
||||
return await this.proxySetTimeouts(url, method, body);
|
||||
case 'setWindow':
|
||||
return await this.proxySetWindow(url, method, body);
|
||||
case 'setValue':
|
||||
return await this.proxySetValue(url, method, body);
|
||||
case 'performActions':
|
||||
return await this.proxyPerformActions(url, method, body);
|
||||
case 'releaseActions':
|
||||
return await this.proxyReleaseActions(url, method);
|
||||
case 'setFrame':
|
||||
return await this.proxySetFrame(url, method, body);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Same arguments, but different URLs
|
||||
for (const {commandNames, jsonwpConverter, w3cConverter} of COMMAND_URLS_CONFLICTS) {
|
||||
if (!(commandNames as readonly string[]).includes(commandName)) {
|
||||
continue;
|
||||
}
|
||||
const rewrittenUrl =
|
||||
this.downstreamProtocol === MJSONWP ? jsonwpConverter(url) : w3cConverter(url);
|
||||
if (rewrittenUrl === url) {
|
||||
this.log.debug(
|
||||
`Did not know how to rewrite the original URL '${url}' for ${this.downstreamProtocol} protocol`
|
||||
);
|
||||
break;
|
||||
}
|
||||
this.log.info(
|
||||
`Rewrote the original URL '${url}' to '${rewrittenUrl}' for ${this.downstreamProtocol} protocol`
|
||||
);
|
||||
return await this.proxyFunc(rewrittenUrl, method, body);
|
||||
}
|
||||
|
||||
// No matches found. Proceed normally
|
||||
return await this.proxyFunc(url, method, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* W3C /timeouts can take as many as 3 timeout types at once, MJSONWP /timeouts only takes one
|
||||
* at a time. So if we're using W3C and proxying to MJSONWP and there's more than one timeout type
|
||||
* provided in the request, we need to do 3 proxies and combine the result.
|
||||
*/
|
||||
private getTimeoutRequestObjects(body: HTTPBody): Record<string, unknown>[] {
|
||||
if (_.isNil(body)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const bodyObj = (util.safeJsonParse(body) as Record<string, unknown>) ?? {};
|
||||
if (this.downstreamProtocol === W3C && _.has(bodyObj, 'ms') && _.has(bodyObj, 'type')) {
|
||||
const typeToW3C = (x: string) => (x === 'page load' ? 'pageLoad' : x);
|
||||
return [
|
||||
{
|
||||
[typeToW3C(bodyObj.type as string)]: bodyObj.ms,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (this.downstreamProtocol === MJSONWP && (!_.has(bodyObj, 'ms') || !_.has(bodyObj, 'type'))) {
|
||||
const typeToJSONWP = (x: string) => (x === 'pageLoad' ? 'page load' : x);
|
||||
return _.toPairs(bodyObj)
|
||||
// Only transform the entry if ms value is a valid positive float number
|
||||
.filter((pair) => /^\d+(?:[.,]\d*?)?$/.test(`${pair[1]}`))
|
||||
.map((pair) => ({
|
||||
type: typeToJSONWP(pair[0]),
|
||||
ms: pair[1],
|
||||
}));
|
||||
}
|
||||
|
||||
return [bodyObj];
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy an array of timeout objects and merge the result.
|
||||
*/
|
||||
private async proxySetTimeouts(
|
||||
url: string,
|
||||
method: string,
|
||||
body?: HTTPBody
|
||||
): Promise<[ProxyResponse, HTTPBody]> {
|
||||
const timeoutRequestObjects = this.getTimeoutRequestObjects(body);
|
||||
if (timeoutRequestObjects.length === 0) {
|
||||
return await this.proxyFunc(url, method, body);
|
||||
}
|
||||
this.log.debug(
|
||||
`Will send the following request bodies to /timeouts: ${JSON.stringify(timeoutRequestObjects)}`
|
||||
);
|
||||
|
||||
let response!: ProxyResponse;
|
||||
let resBody!: HTTPBody;
|
||||
for (const timeoutObj of timeoutRequestObjects) {
|
||||
[response, resBody] = await this.proxyFunc(url, method, timeoutObj as HTTPBody);
|
||||
|
||||
// If we got a non-MJSONWP response, return the result, nothing left to do
|
||||
if (this.downstreamProtocol !== MJSONWP) {
|
||||
return [response, resBody];
|
||||
}
|
||||
// If we got an error, return the error right away
|
||||
if (response.statusCode >= 400) {
|
||||
return [response, resBody];
|
||||
}
|
||||
// ...Otherwise, continue to the next timeouts call
|
||||
}
|
||||
return [response, resBody];
|
||||
}
|
||||
|
||||
private async proxySetWindow(
|
||||
url: string,
|
||||
method: string,
|
||||
body: HTTPBody
|
||||
): Promise<[ProxyResponse, HTTPBody]> {
|
||||
const bodyObj = util.safeJsonParse(body);
|
||||
if (_.isPlainObject(bodyObj)) {
|
||||
const obj = bodyObj as Record<string, unknown>;
|
||||
if (this.downstreamProtocol === W3C && _.has(bodyObj, 'name') && !_.has(bodyObj, 'handle')) {
|
||||
this.log.debug(`Copied 'name' value '${obj.name}' to 'handle' as per W3C spec`);
|
||||
return await this.proxyFunc(url, method, {...obj, handle: obj.name});
|
||||
}
|
||||
if (
|
||||
this.downstreamProtocol === MJSONWP &&
|
||||
_.has(bodyObj, 'handle') &&
|
||||
!_.has(bodyObj, 'name')
|
||||
) {
|
||||
this.log.debug(`Copied 'handle' value '${obj.handle}' to 'name' as per JSONWP spec`);
|
||||
return await this.proxyFunc(url, method, {...obj, name: obj.handle});
|
||||
}
|
||||
}
|
||||
return await this.proxyFunc(url, method, body);
|
||||
}
|
||||
|
||||
private async proxySetValue(
|
||||
url: string,
|
||||
method: string,
|
||||
body: HTTPBody
|
||||
): Promise<[ProxyResponse, HTTPBody]> {
|
||||
const bodyObj = util.safeJsonParse(body) as Record<string, unknown> | undefined;
|
||||
if (_.isPlainObject(bodyObj) && (util.hasValue(bodyObj?.text) || util.hasValue(bodyObj?.value))) {
|
||||
let {text, value} = bodyObj;
|
||||
if (util.hasValue(text) && !util.hasValue(value)) {
|
||||
value = _.isString(text) ? [...text] : _.isArray(text) ? text : [];
|
||||
this.log.debug(`Added 'value' property to 'setValue' request body`);
|
||||
} else if (!util.hasValue(text) && util.hasValue(value)) {
|
||||
text = _.isArray(value) ? value.join('') : _.isString(value) ? value : '';
|
||||
this.log.debug(`Added 'text' property to 'setValue' request body`);
|
||||
}
|
||||
return await this.proxyFunc(url, method, {...bodyObj, text, value});
|
||||
}
|
||||
return await this.proxyFunc(url, method, body);
|
||||
}
|
||||
|
||||
private async proxySetFrame(
|
||||
url: string,
|
||||
method: string,
|
||||
body: HTTPBody
|
||||
): Promise<[ProxyResponse, HTTPBody]> {
|
||||
const bodyObj = util.safeJsonParse(body);
|
||||
if (_.has(bodyObj, 'id') && _.isPlainObject(bodyObj.id)) {
|
||||
return await this.proxyFunc(url, method, {
|
||||
...(bodyObj as object),
|
||||
id: duplicateKeys(bodyObj.id as object, MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY),
|
||||
});
|
||||
}
|
||||
return await this.proxyFunc(url, method, body);
|
||||
}
|
||||
|
||||
private async proxyPerformActions(
|
||||
url: string,
|
||||
method: string,
|
||||
body: HTTPBody
|
||||
): Promise<[ProxyResponse, HTTPBody]> {
|
||||
const bodyObj = util.safeJsonParse(body);
|
||||
if (_.isPlainObject(bodyObj)) {
|
||||
return await this.proxyFunc(
|
||||
url,
|
||||
method,
|
||||
duplicateKeys(bodyObj as object, MJSONWP_ELEMENT_KEY, W3C_ELEMENT_KEY)
|
||||
);
|
||||
}
|
||||
return await this.proxyFunc(url, method, body);
|
||||
}
|
||||
|
||||
private async proxyReleaseActions(
|
||||
url: string,
|
||||
method: string
|
||||
): Promise<[ProxyResponse, HTTPBody]> {
|
||||
return await this.proxyFunc(url, method);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '../protocol/errors';
|
||||
import {isSessionCommand, routeToCommandName} from '../protocol';
|
||||
import {MAX_LOG_BODY_LENGTH, DEFAULT_BASE_PATH, PROTOCOLS} from '../constants';
|
||||
import ProtocolConverter from './protocol-converter';
|
||||
import {ProtocolConverter} from './protocol-converter';
|
||||
import {formatResponseValue, ensureW3cResponse} from '../protocol/helpers';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/base-driver",
|
||||
"version": "10.2.1",
|
||||
"version": "10.2.2",
|
||||
"description": "Base driver class for Appium drivers",
|
||||
"keywords": [
|
||||
"automation",
|
||||
|
||||
@@ -2,14 +2,24 @@ import chai, {expect} from 'chai';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
import _ from 'lodash';
|
||||
import {PROTOCOLS} from '../../../lib/constants';
|
||||
import ProtocolConverter, {
|
||||
import {
|
||||
COMMAND_URLS_CONFLICTS,
|
||||
ProtocolConverter,
|
||||
} from '../../../lib/jsonwp-proxy/protocol-converter';
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
const {MJSONWP, W3C} = PROTOCOLS;
|
||||
|
||||
/**
|
||||
* Type used to access private methods in unit tests.
|
||||
* Standalone type (not intersecting with ProtocolConverter) to avoid TS reducing the intersection to never.
|
||||
*/
|
||||
interface ProtocolConverterTest {
|
||||
getTimeoutRequestObjects(body: unknown): Record<string, unknown>[];
|
||||
proxySetValue(url: string, method: string, body: unknown): Promise<[unknown, unknown]>;
|
||||
}
|
||||
|
||||
describe('Protocol Converter', function () {
|
||||
describe('getTimeoutRequestObjects', function () {
|
||||
let converter: ProtocolConverter;
|
||||
@@ -18,13 +28,15 @@ describe('Protocol Converter', function () {
|
||||
});
|
||||
it('should take W3C inputs and produce MJSONWP compatible objects', function () {
|
||||
converter.downstreamProtocol = MJSONWP;
|
||||
const timeoutObjects = converter.getTimeoutRequestObjects({script: 100});
|
||||
const timeoutObjects = (converter as unknown as ProtocolConverterTest).getTimeoutRequestObjects({
|
||||
script: 100,
|
||||
});
|
||||
expect(timeoutObjects.length).to.equal(1);
|
||||
expect(timeoutObjects[0]).to.eql({type: 'script', ms: 100});
|
||||
});
|
||||
it('should ignore invalid entries while converting from W3C', function () {
|
||||
converter.downstreamProtocol = MJSONWP;
|
||||
const timeoutObjects = converter.getTimeoutRequestObjects({
|
||||
const timeoutObjects = (converter as unknown as ProtocolConverterTest).getTimeoutRequestObjects({
|
||||
script: 100,
|
||||
sessionId: '5432a4f3-cd89-4781-8905-ea9d3150840c',
|
||||
bar: -1,
|
||||
@@ -35,8 +47,9 @@ describe('Protocol Converter', function () {
|
||||
});
|
||||
it('should take multiple W3C timeouts and produce multiple MJSONWP compatible objects', function () {
|
||||
converter.downstreamProtocol = MJSONWP;
|
||||
const [scriptTimeout, pageLoadTimeout, implicitTimeout] =
|
||||
converter.getTimeoutRequestObjects({
|
||||
const [scriptTimeout, pageLoadTimeout, implicitTimeout] = (
|
||||
converter as unknown as ProtocolConverterTest
|
||||
).getTimeoutRequestObjects({
|
||||
script: 100,
|
||||
pageLoad: 200,
|
||||
implicit: 300,
|
||||
@@ -56,7 +69,7 @@ describe('Protocol Converter', function () {
|
||||
});
|
||||
it('should take MJSONWP input and produce W3C compatible object', function () {
|
||||
converter.downstreamProtocol = W3C;
|
||||
const timeoutObjects = converter.getTimeoutRequestObjects({
|
||||
const timeoutObjects = (converter as unknown as ProtocolConverterTest).getTimeoutRequestObjects({
|
||||
type: 'implicit',
|
||||
ms: 300,
|
||||
});
|
||||
@@ -65,7 +78,7 @@ describe('Protocol Converter', function () {
|
||||
});
|
||||
it('should not change the input if protocol name is unknown', function () {
|
||||
converter.downstreamProtocol = null as any;
|
||||
const timeoutObjects = converter.getTimeoutRequestObjects({
|
||||
const timeoutObjects = (converter as unknown as ProtocolConverterTest).getTimeoutRequestObjects({
|
||||
type: 'implicit',
|
||||
ms: 300,
|
||||
});
|
||||
@@ -74,7 +87,7 @@ describe('Protocol Converter', function () {
|
||||
});
|
||||
it('should not change the input if protocol name is unchanged', function () {
|
||||
converter.downstreamProtocol = MJSONWP;
|
||||
const timeoutObjects = converter.getTimeoutRequestObjects({
|
||||
const timeoutObjects = (converter as unknown as ProtocolConverterTest).getTimeoutRequestObjects({
|
||||
type: 'implicit',
|
||||
ms: 300,
|
||||
});
|
||||
@@ -97,7 +110,7 @@ describe('Protocol Converter', function () {
|
||||
});
|
||||
|
||||
it('should calculate value if not present', async function () {
|
||||
await converter.proxySetValue('', '', {
|
||||
await (converter as unknown as ProtocolConverterTest).proxySetValue('', '', {
|
||||
text: 'bla',
|
||||
});
|
||||
expect(responseBody).to.eql({
|
||||
@@ -106,7 +119,7 @@ describe('Protocol Converter', function () {
|
||||
});
|
||||
});
|
||||
it('should calculate text if not present', async function () {
|
||||
await converter.proxySetValue('', '', {
|
||||
await (converter as unknown as ProtocolConverterTest).proxySetValue('', '', {
|
||||
value: ['b', 'l', 'a'],
|
||||
});
|
||||
expect(responseBody).to.eql({
|
||||
@@ -115,7 +128,7 @@ describe('Protocol Converter', function () {
|
||||
});
|
||||
});
|
||||
it('should keep the response body unchanged if both value and text are present', async function () {
|
||||
await converter.proxySetValue('', '', {
|
||||
await (converter as unknown as ProtocolConverterTest).proxySetValue('', '', {
|
||||
text: 'bla',
|
||||
value: ['b', 'l', 'a'],
|
||||
});
|
||||
@@ -130,7 +143,7 @@ describe('Protocol Converter', function () {
|
||||
let w3cConverter: (url: string) => string;
|
||||
before(function () {
|
||||
for (const command of COMMAND_URLS_CONFLICTS) {
|
||||
if (command.commandNames.includes('getProperty')) {
|
||||
if ((command.commandNames as readonly string[]).includes('getProperty')) {
|
||||
jsonwpConverter = command.jsonwpConverter;
|
||||
w3cConverter = command.w3cConverter;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.1.1](https://github.com/appium/appium/compare/@appium/base-plugin@3.1.0...@appium/base-plugin@3.1.1) (2026-03-09)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.1.0](https://github.com/appium/appium/compare/@appium/base-plugin@3.0.6...@appium/base-plugin@3.1.0) (2026-03-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/base-plugin",
|
||||
"version": "3.1.0",
|
||||
"version": "3.1.1",
|
||||
"description": "The base plugin used to create Appium plugins",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -40,7 +40,7 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.*ts\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-driver": "^10.2.1",
|
||||
"@appium/base-driver": "^10.2.2",
|
||||
"@appium/support": "^7.0.6",
|
||||
"@appium/types": "^1.2.1"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [4.1.1](https://github.com/appium/appium/compare/@appium/fake-plugin@4.1.0...@appium/fake-plugin@4.1.1) (2026-03-09)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.1.0](https://github.com/appium/appium/compare/@appium/fake-plugin@4.0.6...@appium/fake-plugin@4.1.0) (2026-03-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/fake-plugin",
|
||||
"version": "4.1.0",
|
||||
"version": "4.1.1",
|
||||
"description": "A fake Appium plugin",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -39,7 +39,7 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.*ts\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-plugin": "^3.1.0",
|
||||
"@appium/base-plugin": "^3.1.1",
|
||||
"@appium/support": "^7.0.6",
|
||||
"asyncbox": "6.1.0",
|
||||
"lodash": "4.17.23"
|
||||
|
||||
Reference in New Issue
Block a user