Merge branch 'master' into appium3

This commit is contained in:
Kazuaki Matsuo
2024-12-27 23:15:42 -08:00
99 changed files with 4277 additions and 5210 deletions

View File

@@ -1,9 +0,0 @@
**/coverage/**
**/node_modules/**
examples/javascript-wd
**/build/**
**/*.min.js
sample-code
**/build-fixtures/**
packages/appium/docs/**/assets/**
packages/appium/docs/**/js/**

View File

@@ -1,62 +0,0 @@
{
"extends": ["@appium/eslint-config-appium-ts"],
"overrides": [
{
"files": "packages/fake-driver/**/*",
"rules": {"require-await": "off"}
},
{"files": "packages/support/**/*", "globals": {"BigInt": "readonly"}},
{
"files": "packages/*/test/**/*",
"rules": {"func-names": "off"}
},
{
"files": [
"packages/appium/support.js",
"packages/appium/driver.js",
"packages/appium/plugin.js"
],
"parserOptions": {
"sourceType": "script"
}
},
{
"files": [
"./test/setup.js",
"./**/scripts/**/*.js",
"./packages/*/index.js",
"./packages/docutils/bin/appium-docs.js"
],
"rules": {
"@typescript-eslint/no-var-requires": "off"
},
"parserOptions": {
"sourceType": "script"
}
},
{
"files": "./packages/*/test/**/*.js",
"rules": {
"@typescript-eslint/no-var-requires": "off",
"no-restricted-properties": [
"error",
{
"object": "sinon",
"property": "spy",
"message": "Use `sandbox = sinon.createSandbox()` and `sandbox.spy()` instead. Don't forget to call `sandbox.restore()` in `afterEach`"
},
{
"object": "sinon",
"property": "stub",
"message": "Use `sandbox = sinon.createSandbox()` and `sandbox.stub()` instead. Don't forget to call `sandbox.restore()` in `afterEach`"
},
{
"object": "sinon",
"property": "mock",
"message": "Use `sandbox = sinon.createSandbox()` and `sandbox.mock()` instead. Don't forget to call `sandbox.restore()` in `afterEach`"
}
]
}
}
]
}

View File

@@ -24,7 +24,7 @@ targets:
- type: npm
path: ./packages/base-driver
- type: npm
path: ./packages/eslint-config-appium
path: ./packages/eslint-config-appium-ts
- type: npm
path: ./packages/fake-driver
- type: npm

5
.github/labeler.yml vendored
View File

@@ -63,11 +63,6 @@ labels:
matcher:
files: ['packages/driver-test-support/**']
- label: '@appium/eslint-config-appium'
sync: true
matcher:
files: ['packages/eslint-config-appium/**']
- label: '@appium/eslint-config-appium-ts'
sync: true
matcher:

View File

@@ -44,6 +44,9 @@ on:
- '!**/sample-code/**'
- '!packages/*/docs/**'
env:
CI: true
permissions:
contents: read

23
eslint.config.mjs Normal file
View File

@@ -0,0 +1,23 @@
import tsConfig from '@appium/eslint-config-appium-ts';
export default [
...tsConfig,
{
...tsConfig.find(({name}) => name === 'Test Files'),
name: 'Test Support',
files: [
'packages/test-support/lib/**',
'packages/driver-test-support/lib/**',
'packages/plugin-test-support/lib/**',
],
},
{
ignores: [
'**/build-fixtures/**',
'packages/appium/docs/**/assets/**',
'packages/appium/docs/**/js/**',
'packages/appium/sample-code/**',
],
},
];

7175
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -85,12 +85,14 @@
},
"devDependencies": {
"@colors/colors": "1.6.0",
"@eslint/eslintrc": "3.1.0",
"@eslint/js": "9.10.0",
"@tsconfig/node14": "14.1.2",
"@types/chai": "5.0.1",
"@types/chai-as-promised": "8.0.1",
"@types/diff": "6.0.0",
"@types/mocha": "10.0.10",
"@types/node": "22.10.1",
"@types/node": "22.10.2",
"@types/semver": "7.5.8",
"@types/sinon": "17.0.3",
"@types/sinon-chai": "4.0.0",
@@ -98,20 +100,19 @@
"@types/teen_process": "2.0.4",
"@types/ws": "8.5.13",
"@types/yargs": "17.0.33",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"@typescript-eslint/eslint-plugin": "8.18.2",
"@typescript-eslint/parser": "8.18.2",
"asyncbox": "3.0.0",
"chai": "5.1.2",
"chai-as-promised": "8.0.1",
"conventional-changelog-conventionalcommits": "7.0.2",
"cpy-cli": "5.0.0",
"eslint": "8.57.1",
"eslint": "9.17.0",
"eslint-config-prettier": "9.1.0",
"eslint-find-rules": "4.2.0",
"eslint-import-resolver-typescript": "3.7.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-mocha": "10.5.0",
"eslint-plugin-promise": "6.6.0",
"eslint-plugin-promise": "7.2.1",
"finalhandler": "1.3.1",
"get-port": "5.1.1",
"json-schema-to-typescript": "15.0.3",
@@ -132,7 +133,7 @@
"tsd": "0.31.2",
"typescript": "5.7.2",
"validate.js": "0.13.1",
"webdriverio": "8.40.6",
"webdriverio": "9.4.5",
"ws": "8.18.0",
"yaml-js": "0.3.1"
},

View File

@@ -324,6 +324,10 @@ Currently, you also need to define a `doesSupportBidi` field on your driver inst
it is set to `true`. Appium will not turn on its Websocket servers for your driver and set up any
handlers unless your driver says that it supports BiDi in this way.
You are not limited to BiDi commands that are defined in the official BiDi specification. If you
wish to define new commands, you may do so; you just need to tell Appium about them! See
[below](#extend-the-existing-protocol-with-new-commands) for more information.
### Implement element finding
Element finding is a special command implementation case. You don't actually want to override
@@ -574,9 +578,10 @@ to the upstream connection.
You may find that the existing commands don't cut it for your driver. If you want to expose
behaviours that don't map to any of the existing commands, you can create new commands in one of
two ways:
three ways:
1. Extending the WebDriver protocol and creating client-side plugins to access the extensions
1. Extending the classic WebDriver protocol and creating client-side plugins to access the extensions via the classic HTTP interface
1. Extending the WebDriver BiDi protocol with new modules and methods, accessed from a client via the BiDi interface
1. Overloading the Execute Script command by defining [Execute
Methods](../guides/execute-methods.md)
@@ -606,7 +611,39 @@ won't have nice client-side functions designed to target these endpoints. So you
create and release client-side plugins for each language you want to support (directions or
examples can be found at the relevant client docs).
An alternative to this way of doing things is to overload a command which all WebDriver clients
The second way of adding new commands is adding them as BiDi commands (accessed via the BiDi
websocket interface, rather than the classic HTTP interface). BiDi commands come in two parts:
a "module", which is basically a container or namespace, and a "command", which is the name of your
new command.
As with the first method, you teach Appium to recognize your new BiDi commands by adding a static
field to your driver class, called `newBidiCommands`. It has a format similar to `newMethodMap`.
Basically it encapsulates information about the BiDi module, BiDi command name, reference to your
driver instance method that will handle the command, and required and optional parameters. Here's
an example of a `newBidiCommands` as implemented on an imaginary driver:
```js
static newBidiCommands = {
video: {
startFramerateCapture: {
command: 'startFrameCap',
params: {
required: ['videoSource'],
optional: ['showOnScreen'],
}
},
stopFramerateCapture: {
command: 'stopFrameCap',
},
}
};
```
In this imaginary example, we have defined two new BiDi commands: `video.startFramerateCapture` and
`video.stopFramerateCapture`. The first command takes a required and an optional parameter, and the
second does not. When combined with generic BiDi support in your driver (see [the section on BiDi](#implement-webdriver-bidi-commands) above), and given an implementation of the appropriate methods on your driver (e.g. `startFrameCap` and `stopFrameCap` in this example), clients would be able to send these BiDi commands using whatever mechanism normally exists for doing so in the client library.
An alternative to these other ways of doing things is to overload a command which all WebDriver clients
have access to already: Execute Script. Appium provides some a convenient tool for making this
easy. Let's say you are building a driver for stereo system called `soundz`, and you wanted to
create a command for playing a song by name. You could expose this to your users in such a way that
@@ -657,6 +694,10 @@ A couple notes about this system:
1. The `executeMethod` helper will reject with an error if a script name doesn't match one of the
script names defined as a command in `executeMethodMap`, or if there are missing parameters.
One of the nice things about the Execute Method strategy is that methods implemented in this way
will be available via the classic or BiDi interfaces (since they will result in the same Appium
handlers being called).
### Build Appium Doctor checks

View File

@@ -32,7 +32,7 @@ Here is a list of all the globally-recognized Appium capabilities:
!!! info
Individual drivers and plugins can support other capabilities, so refer to their documentation
for lists ofspecific capability names. Some drivers may also not support all of these capabilities
for lists of specific capability names. Some drivers may also not support all of these capabilities
| <div style="width:12em">Capability</div> | Type | Required? | Description |
|--------------------------------------------|-----------|-----------|----------------------------|
@@ -67,6 +67,16 @@ Each Appium client has its own way of constructing capabilities and starting a s
examples of doing this in each client library, head to the [Ecosystem](../ecosystem/index.md) page
and click through to the appropriate client documentation.
## BiDi Protocol Support
Appium supports [WebDriver BiDi](https://w3c.github.io/webdriver-bidi/) protocol since basedriver 9.5.0.
The actual behavior depends on individual drivers while the Appium and the baseーdriver support the protocol.
Please make sure if a driver supports the protocol and what kind of commands/events it supports in the documentation.
| Capability Name | Type | Description |
|---|---|--|
| `webSocketUrl` | `boolean` | To enable BiDi protocol in the session. |
## Using `appium:options` to Group Capabilities
If you use a lot of `appium:` capabilities in your tests, it can get a little repetitive. You can

View File

@@ -1,6 +1,4 @@
/* eslint-disable no-unused-vars */
import _ from 'lodash';
import B from 'bluebird';
import {getBuildInfo, updateBuildInfo, APPIUM_VER} from './config';
import {
BaseDriver,
@@ -10,7 +8,6 @@ import {
CREATE_SESSION_COMMAND,
DELETE_SESSION_COMMAND,
GET_STATUS_COMMAND,
MAX_LOG_BODY_LENGTH,
promoteAppiumOptions,
promoteAppiumOptionsForObject,
} from '@appium/base-driver';
@@ -19,20 +16,12 @@ import {
parseCapsForInnerDriver,
pullSettings,
makeNonW3cCapsError,
isBroadcastIp,
fetchInterfaces,
V4_BROADCAST_IP,
validateFeatures,
} from './utils';
import {util, node, logger} from '@appium/support';
import {getDefaultsForExtension} from './schema';
import {DRIVER_TYPE, BIDI_BASE_PATH, BIDI_EVENT_NAME} from './constants';
import WebSocket from 'ws';
import os from 'node:os';
const MIN_WS_CODE_VAL = 1000;
const MAX_WS_CODE_VAL = 1015;
const WS_FALLBACK_CODE = 1011; // server encountered an error while fulfilling request
import {DRIVER_TYPE, BIDI_BASE_PATH} from './constants';
import * as bidiHelpers from './bidi';
const desiredCapabilityConstraints = /** @type {const} */ ({
automationName: {
@@ -178,7 +167,6 @@ class AppiumDriver extends DriverCore {
return this.sessions[sessionId];
}
// eslint-disable-next-line require-await
async getStatus() {
// https://www.w3.org/TR/webdriver/#dfn-status
const statusObj = this._isShuttingDown
@@ -265,347 +253,6 @@ class AppiumDriver extends DriverCore {
}
}
/**
* Initialize a new bidi connection and set up handlers
* @param {import('ws').WebSocket} ws The websocket connection object
* @param {import('http').IncomingMessage} req The connection pathname, which might include the session id
*/
onBidiConnection(ws, req) {
// TODO put bidi-related functionality into a mixin/helper class
// wrap all of the handler logic with exception handling so if something blows up we can log
// and close the websocket
try {
const {bidiHandlerDriver, proxyClient, send, sendToProxy, logSocketErr} = this.initBidiSocket(
ws,
req,
);
this.initBidiSocketHandlers(
ws,
proxyClient,
send,
sendToProxy,
bidiHandlerDriver,
logSocketErr,
);
this.initBidiProxyHandlers(proxyClient, ws, send);
this.initBidiEventListeners(ws, bidiHandlerDriver, send);
} catch (err) {
this.log.error(err);
try {
ws.close();
} catch (ign) {}
}
}
/**
* Initialize a new bidi connection
* @param {import('ws').WebSocket} ws The websocket connection object
* @param {import('http').IncomingMessage} req The connection pathname, which might include the session id
*/
initBidiSocket(ws, req) {
let outOfBandErrorPrefix = '';
const pathname = req.url;
if (!pathname) {
throw new Error('Invalid connection request: pathname missing from request');
}
const bidiSessionRe = new RegExp(`${BIDI_BASE_PATH}/([^/]+)$`);
const bidiNoSessionRe = new RegExp(`${BIDI_BASE_PATH}/?$`);
const sessionMatch = bidiSessionRe.exec(pathname);
const noSessionMatch = bidiNoSessionRe.exec(pathname);
if (!sessionMatch && !noSessionMatch) {
throw new Error(
`Got websocket connection for path ${pathname} but didn't know what to do with it. ` +
`Ignoring and will close the connection`,
);
}
// Let's figure out which driver is going to handle this socket connection. It's either going
// to be a driver matching a session id appended to the bidi base path, or this umbrella driver
// (if no session id is included in the bidi connection request)
/** @type {import('@appium/types').ExternalDriver | AppiumDriver} */
let bidiHandlerDriver;
/** @type {import('ws').WebSocket | null} */
let proxyClient = null;
if (sessionMatch) {
// If we found a session id, see if it matches an active session
const sessionId = sessionMatch[1];
bidiHandlerDriver = this.sessions[sessionId];
if (!bidiHandlerDriver) {
// The session ID sent in doesn't match an active session; just ignore this socket
// connection in that case
throw new Error(
`Got bidi connection request for session with id ${sessionId} which is closed ` +
`or does not exist. Closing the socket connection.`,
);
}
const driverName = bidiHandlerDriver.constructor.name;
outOfBandErrorPrefix = `[session ${sessionId}] `;
this.log.info(`Bidi websocket connection made for session ${sessionId}`);
// store this socket connection for later removal on session deletion. theoretically there
// can be multiple sockets per session
if (!this.bidiSockets[sessionId]) {
this.bidiSockets[sessionId] = [];
}
this.bidiSockets[sessionId].push(ws);
const bidiProxyUrl = bidiHandlerDriver.bidiProxyUrl;
if (bidiProxyUrl) {
try {
new URL(bidiProxyUrl);
} catch (ign) {
throw new Error(
`Got request for ${driverName} to proxy bidi connections to upstream socket with ` +
`url ${bidiProxyUrl}, but this was not a valid url`,
);
}
this.log.info(`Bidi connection for ${driverName} will be proxied to ${bidiProxyUrl}`);
proxyClient = new WebSocket(bidiProxyUrl);
this.bidiProxyClients[sessionId] = proxyClient;
}
} else {
this.log.info('Bidi websocket connection made to main server');
// no need to store the socket connection if it's to the main server since it will just
// stay open as long as the server itself is and will close when the server closes.
bidiHandlerDriver = this; // eslint-disable-line @typescript-eslint/no-this-alias
}
const logSocketErr = (/** @type {Error} */ err) =>
this.log.error(`${outOfBandErrorPrefix}${err}`);
// This is a function which wraps the 'send' method on a web socket for two reasons:
// 1. Make it async-await friendly
// 2. Do some logging if there's a send error
const sendFactory = (/** @type {import('ws').WebSocket} */ socket) => {
const socketSend = B.promisify(socket.send, {context: socket});
return async (/** @type {string|Buffer} */ data) => {
try {
await socketSend(data);
} catch (err) {
logSocketErr(err);
}
};
};
// Construct our send method for sending messages to the client
const send = sendFactory(ws);
// Construct a conditional send method for proxying messages from the client to an upstream
// bidi socket server (e.g. on a browser)
const sendToProxy = proxyClient ? sendFactory(proxyClient) : null;
return {bidiHandlerDriver, proxyClient, send, sendToProxy, logSocketErr};
}
/**
* Set up handlers on upstream bidi socket we are proxying to/from
*
* @param {import('ws').WebSocket | null} proxyClient - the websocket connection to/from the
* upstream socket (the one we're proxying to/from)
* @param {import('ws').WebSocket} ws - the websocket connection to/from the client
* @param {(data: string | Buffer) => Promise<void>} send - a method used to send data to the
* client
*/
initBidiProxyHandlers(proxyClient, ws, send) {
// Set up handlers for events that might come from the upstream bidi socket connection if
// we're in proxy mode
if (proxyClient) {
// Here we're receiving a message from the upstream socket server. We want to pass it on to
// the client
proxyClient.on('message', async (/** @type {Buffer|string} */ data) => {
const logData = _.truncate(data.toString('utf8'), {length: MAX_LOG_BODY_LENGTH});
this.log.debug(
`<-- BIDI Received data from proxied bidi socket, sending to client. Data: ${logData}`,
);
await send(data);
});
// If the upstream socket server closes the connection, should close the connection to the
// client as well
proxyClient.on('close', (code, reason) => {
this.log.debug(
`Upstream bidi socket closed connection (code ${code}, reason: '${reason}'). ` +
`Closing proxy connection to client`,
);
if (!_.isNumber(code)) {
code = parseInt(code, 10);
}
if (_.isNaN(code) || code < MIN_WS_CODE_VAL || code > MAX_WS_CODE_VAL) {
this.log.warn(
`Received code ${code} from upstream socket, but this is not a valid ` +
`websocket code. Rewriting to ${WS_FALLBACK_CODE} for ws compatibility`,
);
code = WS_FALLBACK_CODE;
}
ws.close(code, reason);
});
proxyClient.on('error', (err) => {
this.log.error(`Got error on upstream bidi socket connection: ${err}`);
});
}
}
/**
* Set up handlers on the bidi socket connection to the client
*
* @param {import('ws').WebSocket} ws - the websocket connection to/from the client
* @param {import('ws').WebSocket | null} proxyClient - the websocket connection to/from the
* upstream socket (the one we're proxying to/from, if we're proxying)
* @param {(data: string | Buffer) => Promise<void>} send - a method used to send data to the
* client
* @param {((data: string | Buffer) => Promise<void>) | null} sendToProxy - a method used to send data to the
* upstream socket
* @param {import('@appium/types').ExternalDriver | AppiumDriver} bidiHandlerDriver - the driver
* handling the bidi commands
* @param {(err: Error) => void} logSocketErr - a special prefixed logger
*/
initBidiSocketHandlers(ws, proxyClient, send, sendToProxy, bidiHandlerDriver, logSocketErr) {
ws.on('error', (err) => {
// Can't do much with random errors on the connection other than log them
logSocketErr(err);
});
ws.on('open', () => {
this.log.info('Bidi websocket connection is now open');
});
// Now set up handlers for the various events that might happen on the websocket connection
// coming from the client
// First is incoming messages from the client
ws.on('message', async (/** @type {Buffer} */ data) => {
if (proxyClient) {
const logData = _.truncate(data.toString('utf8'), {length: MAX_LOG_BODY_LENGTH});
this.log.debug(
`--> BIDI Received data from client, sending to upstream bidi socket. Data: ${logData}`,
);
// if we're meant to proxy to an upstream bidi socket, just do that
// @ts-ignore sendToProxy is never null if proxyClient is truthy, but ts doesn't know
// that
await sendToProxy(data.toString('utf8'));
} else {
const res = await this.onBidiMessage(data, bidiHandlerDriver);
await send(JSON.stringify(res));
}
});
// Next consider if the client closes the socket connection on us
ws.on('close', (code, reason) => {
// Not sure if we need to do anything here if the client closes the websocket connection.
// Probably if a session was started via the socket, and the socket closes, we should end the
// associated session to free up resources. But otherwise, for sockets attached to existing
// sessions, doing nothing is probably right.
this.log.debug(`Bidi socket connection closed (code ${code}, reason: '${reason}')`);
// If we're proxying, might as well close the upstream connection and clean it up
if (proxyClient) {
this.log.debug('Also closing bidi proxy socket connection');
proxyClient.close(code, reason);
}
});
}
/**
* Set up bidi event listeners
*
* @param {import('ws').WebSocket} ws - the websocket connection to/from the client
* @param {import('@appium/types').ExternalDriver | AppiumDriver} bidiHandlerDriver - the driver
* handling the bidi commands
* @param {(data: string | Buffer) => Promise<void>} send - a method used to send data to the
* client
*/
initBidiEventListeners(ws, bidiHandlerDriver, send) {
// If the driver emits a bidi event that should maybe get sent to the client, check to make
// sure the client is subscribed and then pass it on
let eventListener = async ({context, method, params}) => {
// if the driver didn't specify a context, use the empty context
if (!context) {
context = '';
}
if (!method || !params) {
throw new Error(
`Driver emitted a bidi event that was malformed. Require method and params keys ` +
`(with optional context). But instead received: ${JSON.stringify({
context,
method,
params,
})}`,
);
}
if (ws.readyState !== WebSocket.OPEN) {
// if the websocket is not still 'open', then we can ignore sending these events
if (ws.readyState > WebSocket.OPEN) {
// if the websocket is closed or closing, we can remove this listener as well to avoid
// leaks
bidiHandlerDriver.eventEmitter.removeListener(BIDI_EVENT_NAME, eventListener);
}
return;
}
if (bidiHandlerDriver.bidiEventSubs[method]?.includes(context)) {
this.log.info(
`<-- BIDI EVENT ${method} (context: '${context}', params: ${JSON.stringify(params)})`,
);
// now we can send the event onto the socket
const ev = {type: 'event', context, method, params};
await send(JSON.stringify(ev));
}
};
bidiHandlerDriver.eventEmitter.on(BIDI_EVENT_NAME, eventListener);
}
/**
* @param {Buffer} data
* @param {ExternalDriver | AppiumDriver} driver
*/
async onBidiMessage(data, driver) {
let resMessage, id, method, params;
const dataTruncated = _.truncate(data.toString(), {length: 100});
try {
try {
({id, method, params} = JSON.parse(data.toString('utf8')));
} catch (err) {
throw new errors.InvalidArgumentError(
`Could not parse Bidi command '${dataTruncated}': ${err.message}`,
);
}
driver.log.info(`--> BIDI message #${id}`);
if (!method) {
throw new errors.InvalidArgumentError(
`Missing method for BiDi operation in '${dataTruncated}'`,
);
}
if (!params) {
throw new errors.InvalidArgumentError(
`Missing params for BiDi operation in '${dataTruncated}`,
);
}
const result = await driver.executeBidiCommand(method, params);
// https://w3c.github.io/webdriver-bidi/#protocol-definition
resMessage = {
id,
type: 'success',
result,
};
} catch (err) {
resMessage = err.bidiErrObject(id);
}
driver.log.info(`<-- BIDI message #${id}`);
return resMessage;
}
/**
* Log a bidi server error
* @param {Error} err
*/
onBidiServerError(err) {
this.log.error(`Error from bidi websocket server: ${err}`);
}
/**
* Create a new session
* @param {W3CAppiumDriverCaps} jsonwpCaps JSONWP formatted desired capabilities
@@ -695,6 +342,10 @@ class AppiumDriver extends DriverCore {
driverInstance.relaxedSecurityEnabled = true;
}
// We also want to assign any new Bidi Commands that the driver has specified, including all
// the standard bidi commands
driverInstance.updateBidiCommands(InnerDriver.newBidiCommands ?? {});
if (!_.isEmpty(this.args.denyInsecure)) {
this.log.info('Explicitly preventing use of insecure features:');
this.args.denyInsecure.map((a) => this.log.info(` ${a}`));
@@ -783,7 +434,7 @@ class AppiumDriver extends DriverCore {
if (dCaps.webSocketUrl && driverInstance.doesSupportBidi) {
const {address, port, basePath} = this.args;
const scheme = `ws${this.server.isSecure() ? 's' : ''}`;
const host = determineBiDiHost(address);
const host = bidiHelpers.determineBiDiHost(address);
const bidiUrl = `${scheme}://${host}:${port}${basePath}${BIDI_BASE_PATH}/${innerSessionId}`;
this.log.info(
`Upstream driver responded with webSocketUrl ${dCaps.webSocketUrl}, will rewrite to ` +
@@ -1102,7 +753,9 @@ class AppiumDriver extends DriverCore {
// if we're running with plugins, make sure we log that the default behavior is actually
// happening so we can tell when the plugin call chain is unwrapping to the default behavior
// if that's what happens
plugins.length && this.log.info(`Executing default handling behavior for command '${cmd}'`);
if (plugins.length) {
this.log.info(`Executing default handling behavior for command '${cmd}'`);
}
// if we make it here, we know that the default behavior is handled
cmdHandledBy.default = true;
@@ -1184,8 +837,9 @@ class AppiumDriver extends DriverCore {
}
wrapCommandWithPlugins({driver, cmd, args, next, cmdHandledBy, plugins}) {
plugins.length &&
if (plugins.length) {
this.log.info(`Plugins which can handle cmd '${cmd}': ${plugins.map((p) => p.name)}`);
}
// now we can go through each plugin and wrap `next` around its own handler, passing the *old*
// next in so that it can call it if it wants to
@@ -1276,6 +930,10 @@ class AppiumDriver extends DriverCore {
const dstSession = this.sessions[sessionId];
return dstSession && dstSession.canProxy(sessionId);
}
onBidiConnection = bidiHelpers.onBidiConnection;
onBidiMessage = bidiHelpers.onBidiMessage;
onBidiServerError = bidiHelpers.onBidiServerError;
}
// help decide which commands should be proxied to sub-drivers and which
@@ -1284,25 +942,6 @@ function isAppiumDriverCommand(cmd) {
return !isSessionCommand(cmd) || cmd === DELETE_SESSION_COMMAND;
}
/**
* Clients cannot use broadcast addresses, like 0.0.0.0 or ::
* to create connections. Thus we prefer a hostname if such
* address is provided or the actual address of a non-local interface,
* in case the host only has one such interface.
*
* @param {string} address
* @returns {string}
*/
function determineBiDiHost(address) {
if (!isBroadcastIp(address)) {
return address;
}
const nonLocalInterfaces = fetchInterfaces(address === V4_BROADCAST_IP ? 4 : 6)
.filter((iface) => !iface.internal);
return nonLocalInterfaces.length === 1 ? nonLocalInterfaces[0].address : os.hostname();
}
/**
* Thrown when Appium tried to proxy a command using a driver's `proxyCommand` method but the
* method did not exist

436
packages/appium/lib/bidi.ts Normal file
View File

@@ -0,0 +1,436 @@
import _ from 'lodash';
import B from 'bluebird';
import {
errors,
} from '@appium/base-driver';
import {BIDI_BASE_PATH, BIDI_EVENT_NAME} from './constants';
import WebSocket from 'ws';
import os from 'node:os';
import {
isBroadcastIp,
fetchInterfaces,
V4_BROADCAST_IP,
} from './utils';
import type {IncomingMessage} from 'node:http';
import type {AppiumDriver} from './appium';
import type {
ErrorBiDiCommandResponse,
SuccessBiDiCommandResponse,
ExternalDriver,
StringRecord
} from '@appium/types';
type AnyDriver = ExternalDriver | AppiumDriver;
type SendData = (data: string | Buffer) => Promise<void>;
type LogSocketError = (err: Error) => void;
interface InitBiDiSocketResult {
bidiHandlerDriver: AnyDriver;
proxyClient: WebSocket | null;
send: SendData;
sendToProxy: SendData | null;
logSocketErr: LogSocketError;
}
const MIN_WS_CODE_VAL = 1000;
const MAX_WS_CODE_VAL = 1015;
const WS_FALLBACK_CODE = 1011; // server encountered an error while fulfilling request
const BIDI_EVENTS_MAP: WeakMap<AnyDriver, Record<string, number>> = new WeakMap();
const MAX_LOGGED_DATA_LENGTH = 300;
/**
* Clients cannot use broadcast addresses, like 0.0.0.0 or ::
* to create connections. Thus we prefer a hostname if such
* address is provided or the actual address of a non-local interface,
* in case the host only has one such interface.
*
* @param address
*/
export function determineBiDiHost(address: string): string {
if (!isBroadcastIp(address)) {
return address;
}
const nonLocalInterfaces = fetchInterfaces(address === V4_BROADCAST_IP ? 4 : 6)
.filter((iface) => !iface.internal);
return nonLocalInterfaces.length === 1 ? nonLocalInterfaces[0].address : os.hostname();
}
/**
* Initialize a new bidi connection and set up handlers
* @param ws The websocket connection object
* @param req The connection pathname, which might include the session id
*/
export function onBidiConnection(this: AppiumDriver, ws: WebSocket, req: IncomingMessage): void {
try {
const initBiDiSocketFunc: OmitThisParameter<typeof initBidiSocket> = initBidiSocket.bind(this);
const {bidiHandlerDriver, proxyClient, send, sendToProxy, logSocketErr} = initBiDiSocketFunc(
ws,
req,
);
const initBidiSocketHandlersFunc: OmitThisParameter<typeof initBidiSocketHandlers> = initBidiSocketHandlers
.bind(this);
initBidiSocketHandlersFunc(
ws,
proxyClient,
send,
sendToProxy,
bidiHandlerDriver,
logSocketErr,
);
if (proxyClient) {
const initBidiProxyHandlersFunc: OmitThisParameter<typeof initBidiProxyHandlers> = initBidiProxyHandlers
.bind(bidiHandlerDriver);
initBidiProxyHandlersFunc(proxyClient, ws, send);
}
const initBidiEventListenersFunc: OmitThisParameter<typeof initBidiEventListeners> = initBidiEventListeners
.bind(this);
initBidiEventListenersFunc(ws, bidiHandlerDriver, send);
} catch (err) {
this.log.error(err);
try {
ws.close();
} catch (ign) {}
}
}
/**
* @param data
* @param driver
*/
export async function onBidiMessage(
this: AppiumDriver,
data: Buffer,
driver: AnyDriver
): Promise<SuccessBiDiCommandResponse | ErrorBiDiCommandResponse> {
let resMessage: SuccessBiDiCommandResponse | ErrorBiDiCommandResponse;
let id: number = 0;
const driverLog = driver.log;
const dataTruncated = _.truncate(data.toString(), {length: MAX_LOGGED_DATA_LENGTH});
try {
let method: string;
let params: StringRecord;
try {
({id, method, params} = JSON.parse(data.toString('utf8')));
} catch (err) {
throw new errors.InvalidArgumentError(
`Could not parse Bidi command '${dataTruncated}': ${err.message}`,
);
}
driverLog.info(`--> BIDI message #${id}`);
if (!method) {
throw new errors.InvalidArgumentError(
`Missing method for BiDi operation in '${dataTruncated}'`,
);
}
if (!params) {
throw new errors.InvalidArgumentError(
`Missing params for BiDi operation in '${dataTruncated}`,
);
}
const result = await driver.executeBidiCommand(method, params);
resMessage = {
id,
type: 'success',
result,
};
} catch (err) {
resMessage = _.has(err, 'bidiErrObject')
? err.bidiErrObject(id)
: {
id,
type: 'error',
error: errors.UnknownError.error(),
message: (err as Error).message,
stacktrace: (err as Error).stack,
};
}
driverLog.info(`<-- BIDI message #${id}`);
return resMessage;
}
/**
* Log a bidi server error
* @param err
*/
export function onBidiServerError(this: AppiumDriver, err: Error): void {
this.log.warn(`Error from bidi websocket server: ${err}`);
}
/**
* Initialize a new bidi connection
* @param ws The websocket connection object
* @param req The connection pathname, which might include the session id
*/
function initBidiSocket(this: AppiumDriver, ws: WebSocket, req: IncomingMessage): InitBiDiSocketResult {
const pathname = req.url;
if (!pathname) {
throw new Error('Invalid connection request: pathname missing from request');
}
const bidiSessionRe = new RegExp(`${BIDI_BASE_PATH}/([^/]+)$`);
const bidiNoSessionRe = new RegExp(`${BIDI_BASE_PATH}/?$`);
const sessionMatch = bidiSessionRe.exec(pathname);
const noSessionMatch = bidiNoSessionRe.exec(pathname);
if (!sessionMatch && !noSessionMatch) {
throw new Error(
`Got websocket connection for path ${pathname} but didn't know what to do with it. ` +
`Ignoring and will close the connection`,
);
}
// Let's figure out which driver is going to handle this socket connection. It's either going
// to be a driver matching a session id appended to the bidi base path, or this umbrella driver
// (if no session id is included in the bidi connection request)
let bidiHandlerDriver: AnyDriver;
let proxyClient: WebSocket | null = null;
if (sessionMatch) {
// If we found a session id, see if it matches an active session
const sessionId = sessionMatch[1];
bidiHandlerDriver = this.sessions[sessionId];
if (!bidiHandlerDriver) {
// The session ID sent in doesn't match an active session; just ignore this socket
// connection in that case
throw new Error(
`Got bidi connection request for session with id ${sessionId} which is closed ` +
`or does not exist. Closing the socket connection.`,
);
}
const driverName = bidiHandlerDriver.constructor.name;
this.log.info(`Bidi websocket connection made for session ${sessionId}`);
// store this socket connection for later removal on session deletion. theoretically there
// can be multiple sockets per session
if (!this.bidiSockets[sessionId]) {
this.bidiSockets[sessionId] = [];
}
this.bidiSockets[sessionId].push(ws);
const bidiProxyUrl = bidiHandlerDriver.bidiProxyUrl;
if (bidiProxyUrl) {
try {
new URL(bidiProxyUrl);
} catch (ign) {
throw new Error(
`Got request for ${driverName} to proxy bidi connections to upstream socket with ` +
`url ${bidiProxyUrl}, but this was not a valid url`,
);
}
this.log.info(`Bidi connection for ${driverName} will be proxied to ${bidiProxyUrl}`);
proxyClient = new WebSocket(bidiProxyUrl);
this.bidiProxyClients[sessionId] = proxyClient;
}
} else {
this.log.info('Bidi websocket connection made to main server');
// no need to store the socket connection if it's to the main server since it will just
// stay open as long as the server itself is and will close when the server closes.
bidiHandlerDriver = this; // eslint-disable-line @typescript-eslint/no-this-alias
}
const driverLog = bidiHandlerDriver.log;
const logSocketErr: LogSocketError = (err: Error) => {
driverLog.warn(err.message);
};
// This is a function which wraps the 'send' method on a web socket for two reasons:
// 1. Make it async-await friendly
// 2. Do some logging if there's a send error
const sendFactory = (socket: WebSocket) => {
const socketSend = B.promisify(socket.send, {context: socket});
return async (data: string | Buffer) => {
try {
await socketSend(data);
} catch (err) {
logSocketErr(err);
}
};
};
// Construct our send method for sending messages to the client
const send: SendData = sendFactory(ws);
// Construct a conditional send method for proxying messages from the client to an upstream
// bidi socket server (e.g. on a browser)
const sendToProxy: SendData | null = proxyClient ? sendFactory(proxyClient) : null;
return {bidiHandlerDriver, proxyClient, send, sendToProxy, logSocketErr};
}
/**
* Set up handlers on upstream bidi socket we are proxying to/from
*
* @param proxyClient - the websocket connection to/from the
* upstream socket (the one we're proxying to/from)
* @param ws - the websocket connection to/from the client
* @param send - a method used to send data to the
* client
*/
function initBidiProxyHandlers(
this: AnyDriver,
proxyClient: WebSocket,
ws: WebSocket,
send: SendData,
): void {
// Set up handlers for events that might come from the upstream bidi socket connection if
// we're in proxy mode
const driverLog = this.log;
// Here we're receiving a message from the upstream socket server. We want to pass it on to
// the client
proxyClient.on('message', send);
// If the upstream socket server closes the connection, should close the connection to the
// client as well
proxyClient.on('close', (code, reason) => {
driverLog.debug(
`Upstream bidi socket closed connection (code ${code}, reason: '${reason}'). ` +
`Closing proxy connection to client`,
);
const intCode: number = _.isNumber(code) ? (code as number) : parseInt(code, 10);
if (_.isNaN(intCode) || intCode < MIN_WS_CODE_VAL || intCode > MAX_WS_CODE_VAL) {
driverLog.warn(
`Received code ${code} from upstream socket, but this is not a valid ` +
`websocket code. Rewriting to ${WS_FALLBACK_CODE} for ws compatibility`,
);
code = WS_FALLBACK_CODE;
}
ws.close(code, reason);
});
proxyClient.on('error', (err) => {
driverLog.warn(`Got error on upstream bidi socket connection: ${err.message}`);
});
}
/**
* Set up handlers on the bidi socket connection to the client
*
* @param ws - the websocket connection to/from the client
* @param proxyClient - the websocket connection to/from the
* upstream socket (the one we're proxying to/from, if we're proxying)
* @param send - a method used to send data to the
* client
* @param sendToProxy - a method used to send data to the
* upstream socket
* @param bidiHandlerDriver - the driver
* handling the bidi commands
* @param logSocketErr - a special prefixed logger
*/
function initBidiSocketHandlers(
this: AppiumDriver,
ws: WebSocket,
proxyClient: WebSocket | null,
send: SendData,
sendToProxy: SendData | null,
bidiHandlerDriver: AnyDriver,
logSocketErr: LogSocketError,
): void {
const driverLog = bidiHandlerDriver.log;
// Can't do much with random errors on the connection other than log them
ws.on('error', logSocketErr);
ws.on('open', () => {
driverLog.info('BiDi websocket connection is now open');
});
// Now set up handlers for the various events that might happen on the websocket connection
// coming from the client
// First is incoming messages from the client
ws.on('message', async (data: Buffer) => {
if (proxyClient && sendToProxy) {
// if we're meant to proxy to an upstream bidi socket, just do that
await sendToProxy(data.toString('utf8'));
} else {
const res = await this.onBidiMessage(data, bidiHandlerDriver);
await send(JSON.stringify(res));
}
});
// Next consider if the client closes the socket connection on us
ws.on('close', (code, reason) => {
// Not sure if we need to do anything here if the client closes the websocket connection.
// Probably if a session was started via the socket, and the socket closes, we should end the
// associated session to free up resources. But otherwise, for sockets attached to existing
// sessions, doing nothing is probably right.
driverLog.debug(`BiDi socket connection closed (code ${code}, reason: '${reason}')`);
// If we're proxying, might as well close the upstream connection and clean it up
if (proxyClient) {
driverLog.debug('Also closing BiDi proxy socket connection');
proxyClient.close(code, reason);
}
const eventLogCounts = BIDI_EVENTS_MAP.get(bidiHandlerDriver);
if (!_.isEmpty(eventLogCounts)) {
driverLog.debug(`BiDi events statistics: ${JSON.stringify(eventLogCounts, null, 2)}`);
}
});
}
/**
* Set up bidi event listeners
*
* @param ws - the websocket connection to/from the client
* @param bidiHandlerDriver - the driver
* handling the bidi commands
* @param send - a method used to send data to the
* client
*/
function initBidiEventListeners(
this: AppiumDriver,
ws: WebSocket,
bidiHandlerDriver: AnyDriver,
send: SendData,
): void {
// If the driver emits a bidi event that should maybe get sent to the client, check to make
// sure the client is subscribed and then pass it on
const driverLog = bidiHandlerDriver.log;
const driverEe = bidiHandlerDriver.eventEmitter;
const eventLogCounts: Record<string, number> = BIDI_EVENTS_MAP.get(bidiHandlerDriver) ?? {};
BIDI_EVENTS_MAP.set(bidiHandlerDriver, eventLogCounts);
const eventListener = async ({context, method, params = {}}) => {
// if the driver didn't specify a context, use the empty context
if (!context) {
context = '';
}
if (!method || !params) {
driverLog.warn(
`Driver emitted a bidi event that was malformed. Require method and params keys ` +
`(with optional context). But instead received: ${_.truncate(JSON.stringify({
context,
method,
params,
}), {length: MAX_LOGGED_DATA_LENGTH})}`,
);
return;
}
if (ws.readyState !== WebSocket.OPEN) {
// if the websocket is not still 'open', then we can ignore sending these events
if (ws.readyState > WebSocket.OPEN) {
// if the websocket is closed or closing, we can remove this listener as well to avoid
// leaks
driverEe.removeListener(BIDI_EVENT_NAME, eventListener);
}
return;
}
const eventSubs = bidiHandlerDriver.bidiEventSubs[method];
if (_.isArray(eventSubs) && eventSubs.includes(context)) {
if (method in eventLogCounts) {
++eventLogCounts[method];
} else {
driverLog.info(
`<-- BIDI EVENT ${method} (context: '${context}', ` +
`params: ${_.truncate(JSON.stringify(params), {length: MAX_LOGGED_DATA_LENGTH})}). ` +
`All further similar events won't be logged.`,
);
eventLogCounts[method] = 1;
}
// now we can send the event onto the socket
const ev = {type: 'event', context, method, params};
await send(JSON.stringify(ev));
}
};
driverEe.on(BIDI_EVENT_NAME, eventListener);
}

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-console */
import B from 'bluebird';
import _ from 'lodash';
import path from 'path';
@@ -18,7 +17,7 @@ import {inspect} from 'node:util';
import {pathToFileURL} from 'url';
import {Doctor, EXIT_CODE as DOCTOR_EXIT_CODE} from '../doctor/doctor';
import {npmPackage} from '../utils';
import semver from 'semver';
import * as semver from 'semver';
const UPDATE_ALL = 'installed';
@@ -486,10 +485,11 @@ class ExtensionCliCommand {
getInstallationReceipt({pkg, installPath, installType, installSpec}) {
const {appium, name, version, peerDependencies} = pkg;
const strVersion = /** @type {string} */ (version);
/** @type {import('appium/types').InternalMetadata} */
const internal = {
pkgName: name,
version,
pkgName: /** @type {string} */ (name),
version: strVersion,
installType,
installSpec,
installPath,

View File

@@ -27,7 +27,9 @@ export function errAndQuit(json, msg) {
* @param {string} msg - string to log
*/
export function log(json, msg) {
!json && console.log(msg);
if (!json) {
console.log(msg);
}
}
/**

View File

@@ -3,7 +3,7 @@ import _ from 'lodash';
import {system, fs, npm} from '@appium/support';
import axios from 'axios';
import {exec} from 'teen_process';
import semver from 'semver';
import * as semver from 'semver';
import os from 'node:os';
import {npmPackage} from './utils';
import B from 'bluebird';
@@ -194,7 +194,7 @@ export function checkNodeOk() {
export async function showBuildInfo() {
await updateBuildInfo(true);
console.log(JSON.stringify(getBuildInfo())); // eslint-disable-line no-console
console.log(JSON.stringify(getBuildInfo()));
}
/**

View File

@@ -242,8 +242,8 @@ export class Manifest {
* @type {InternalMetadata}
*/
const internal = {
pkgName: pkgJson.name,
version: pkgJson.version,
pkgName: /** @type {string} */ (pkgJson.name),
version: /** @type {string} */ (pkgJson.version),
appiumVersion: pkgJson.peerDependencies?.appium,
installType,
installSpec: `${pkgJson.name}@${pkgJson.version}`,

View File

@@ -247,7 +247,7 @@ export function adjustNodePath() {
// ! so it could break (maybe, eventually).
// See https://gist.github.com/branneman/8048520#7-the-hack
// @ts-ignore see above comment
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('module').Module._initPaths();
return true;
} catch (e) {

View File

@@ -90,7 +90,7 @@
"semver": "7.6.3",
"source-map-support": "0.5.21",
"teen_process": "2.2.2",
"type-fest": "4.30.0",
"type-fest": "4.31.0",
"winston": "3.17.0",
"wrap-ansi": "7.0.0",
"ws": "8.18.0",

View File

@@ -1,5 +1,5 @@
{
"devDependencies": {
"webdriverio": "8.40.6"
"webdriverio": "9.4.5"
}
}

View File

@@ -3,12 +3,12 @@ import {tempDir, fs} from '@appium/support';
import {exec} from 'teen_process';
import B from 'bluebird';
import {
APPIUM_ROOT,
readAppiumArgErrorFixture,
formatAppiumArgErrorOutput,
EXECUTABLE,
runAppiumRaw,
} from './e2e-helpers';
import {APPIUM_ROOT} from '../helpers';
import { stripColorCodes } from '../../lib/logsink';
describe('argument parsing', function () {

View File

@@ -223,6 +223,11 @@ describe('Driver CLI', function () {
});
it('should install a driver from GitHub', async function () {
if (process.env.CI) {
// This test is too slow for CI env
return this.skip();
}
const ret = await runInstall([
'appium/appium-fake-driver',
'--source',
@@ -257,6 +262,11 @@ describe('Driver CLI', function () {
});
it('should install a driver from a remote git repo', async function () {
if (process.env.CI) {
// This test is too slow for CI env
return this.skip();
}
const ret = await runInstall([
'git+https://github.com/appium/appium-fake-driver.git',
'--source',

View File

@@ -4,7 +4,6 @@ import {BaseDriver} from '@appium/base-driver';
import {exec} from 'teen_process';
import {fs, tempDir} from '@appium/support';
import axios from 'axios';
import {command} from 'webdriver';
import B from 'bluebird';
import _ from 'lodash';
import {createSandbox} from 'sinon';
@@ -110,12 +109,14 @@ describe('FakeDriver via HTTP', function () {
* @param {Partial<import('appium/types').ParsedArgs>} [args]
*/
function withServer(args = {}) {
// eslint-disable-next-line mocha/no-sibling-hooks
before(async function () {
args = {...args, appiumHome, port, address: TEST_HOST};
if (shouldStartServer) {
server = await appiumServer(args);
}
});
// eslint-disable-next-line mocha/no-sibling-hooks
after(async function () {
if (server) {
await server.close();
@@ -251,8 +252,6 @@ describe('FakeDriver via HTTP', function () {
await B.delay(250);
await driver.getPageSource().should.eventually.be.rejectedWith(/terminated/);
await driver.getSessions().should.eventually.be.empty;
});
it('should not allow umbrella commands to prevent newCommandTimeout on inner driver', async function () {
@@ -264,6 +263,10 @@ describe('FakeDriver via HTTP', function () {
);
let driver = await wdio({...wdOpts, capabilities: localCaps});
should.exist(driver.sessionId);
driver.addCommand(
'getSessions',
async () => (await axios.get(`${testServerBaseUrl}/sessions`)).data.value
);
// get the session list 6 times over 300ms. each request will be below the new command
// timeout but since they are not received by the driver the session should still time out
@@ -506,51 +509,12 @@ describe('FakeDriver via HTTP', function () {
await driver.deleteSession();
}
});
it.skip('should log a single deprecation warning if a deprecated method is used and not overridden by a newMethodMap', async function () {
let driver = await wdio({...wdOpts, capabilities: caps});
try {
driver.addCommand(
'deprecated',
command('POST', '/session/:sessionId/deprecated', {
command: 'deprecated',
description: 'Call a deprecated command',
parameters: [],
ref: '',
}),
);
driver.addCommand(
'doubleClick',
command('POST', '/session/:sessionId/doubleclick', {
command: 'doubleClick',
description: 'Global double click',
parameters: [],
ref: '',
}),
);
await driver
.executeScript('fake: getDeprecatedCommandsCalled', [])
.should.eventually.eql([]);
await driver.deprecated();
await driver.deprecated();
await driver.shake();
// this call should not trigger a deprecation even though deprecated by appium because it's
// overridden as not deprecated by fake driver
await driver.doubleClick();
await driver
.executeScript('fake: getDeprecatedCommandsCalled', [])
.should.eventually.eql(['callDeprecatedCommand', 'mobileShake']);
} finally {
await driver.deleteSession();
}
});
});
describe('Bidi protocol', function () {
withServer();
const capabilities = {...caps, webSocketUrl: true, 'appium:runClock': true};
/** @type import('webdriverio').Browser **/
let driver;
beforeEach(async function () {
@@ -601,6 +565,14 @@ describe('FakeDriver via HTTP', function () {
await B.delay(750);
collectedEvents.should.be.empty;
});
it('should allow custom bidi commands', async function () {
let {result} = await driver.send({method: 'fake.getFakeThing', params: {}});
should.not.exist(result);
await driver.send({method: 'fake.setFakeThing', params: {thing: 'this is from bidi'}});
({result} = await driver.send({method: 'fake.getFakeThing', params: {}}));
result.should.eql('this is from bidi');
});
});
});
@@ -644,36 +616,41 @@ describe('Bidi over SSL', function () {
let should;
before(async function () {
const chai = await import('chai');
const chaiAsPromised = await import('chai-as-promised');
chai.use(chaiAsPromised.default);
should = chai.should();
// TODO: Unskip after https://github.com/webdriverio/webdriverio/issues/13994 is fixed
return this.skip();
try {
await generateCertificate(certPath, keyPath);
} catch (e) {
if (process.env.CI) {
throw e;
}
return this.skip();
}
sandbox = createSandbox();
appiumHome = await tempDir.openDir();
wdOpts.port = port = await getTestPort();
testServerBaseUrl = `https://${TEST_HOST}:${port}`;
FakeDriver = await initFakeDriver(appiumHome);
server = await appiumServer({
address: TEST_HOST,
port,
appiumHome,
sslCertificatePath: certPath,
sslKeyPath: keyPath,
});
// const chai = await import('chai');
// const chaiAsPromised = await import('chai-as-promised');
// chai.use(chaiAsPromised.default);
// should = chai.should();
// try {
// await generateCertificate(certPath, keyPath);
// } catch (e) {
// if (process.env.CI) {
// throw e;
// }
// return this.skip();
// }
// sandbox = createSandbox();
// appiumHome = await tempDir.openDir();
// wdOpts.port = port = await getTestPort();
// testServerBaseUrl = `https://${TEST_HOST}:${port}`;
// FakeDriver = await initFakeDriver(appiumHome);
// server = await appiumServer({
// address: TEST_HOST,
// port,
// appiumHome,
// sslCertificatePath: certPath,
// sslKeyPath: keyPath,
// });
});
after(async function () {
await fs.rimraf(appiumHome);
await server.close();
if (server) {
await fs.rimraf(appiumHome);
await server.close();
}
});
beforeEach(async function () {

View File

@@ -10,7 +10,7 @@ import {createSandbox} from 'sinon';
import {finalizeSchema, registerSchema, resetSchema} from '../../lib/schema/schema';
import {insertAppiumPrefixes, removeAppiumPrefixes} from '../../lib/utils';
import {rewiremock, BASE_CAPS, W3C_CAPS, W3C_PREFIXED_CAPS} from '../helpers';
import BasePlugin from '@appium/base-plugin';
import {BasePlugin} from '@appium/base-plugin';
const SESSION_ID = '1';
@@ -358,7 +358,7 @@ describe('AppiumDriver', function () {
_.keys(appium.sessions).should.not.contain(sessionId);
});
it('should remove session if inner driver unexpectedly exits with no error', async function () {
let [sessionId] = (await appium.createSession(null, null, _.clone(W3C_CAPS))).value; // eslint-disable-line comma-spacing
let [sessionId] = (await appium.createSession(null, null, _.clone(W3C_CAPS))).value;
_.keys(appium.sessions).should.contain(sessionId);
appium.sessions[sessionId].eventEmitter.emit('onUnexpectedShutdown');
// let event loop spin so rejection is handled

View File

@@ -212,14 +212,11 @@ describe('config-file', function () {
});
describe('when the config file is invalid', function () {
beforeEach(function () {
beforeEach(async function () {
lc.search.resolves({
config: {foo: 'bar'},
filepath: '/path/to/file.json',
});
});
beforeEach(async function () {
result = await readConfigFile();
});

View File

@@ -1,4 +1,4 @@
/* eslint-disable require-await */
// @ts-check
/**

View File

@@ -1,5 +1,3 @@
// @ts-check
import _ from 'lodash';
import {PLUGIN_TYPE} from '../../../lib/constants';
import {finalizeSchema, registerSchema, resetSchema} from '../../../lib/schema';
@@ -305,10 +303,6 @@ describe('cli-args', function () {
/`enum` is only supported for `type: 'string'`/i
);
});
it(
'should actually throw earlier by failing schema validation, but that would mean overriding the behavior of `enum` which sounds inadvisable'
);
});
describe('when used with a `string` type', function () {

View File

@@ -1,6 +1,7 @@
import type {Constraints, Driver, IBidiCommands} from '@appium/types';
import type {Constraints, DriverStatus, IBidiCommands} from '@appium/types';
import type {BaseDriver} from '../driver';
import {mixin} from './mixin';
import _ from 'lodash';
declare module '../driver' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -32,6 +33,19 @@ const BidiCommands: IBidiCommands = {
}
}
},
async bidiStatus<C extends Constraints>(this: BaseDriver<C>): Promise<DriverStatus> {
const result = await this.getStatus();
if (!_.has(result, 'ready')) {
//@ts-ignore This is OK
result.ready = true;
}
if (!_.has(result, 'message')) {
//@ts-ignore This is OK
result.message = `${this.constructor.name} is ready to accept commands`;
}
return result as DriverStatus;
}
};
mixin(BidiCommands);

View File

@@ -5,7 +5,7 @@ import {BaseDriver} from '../driver';
import {mixin} from './mixin';
declare module '../driver' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface BaseDriver<C extends Constraints> extends IFindCommands {}
}

View File

@@ -10,6 +10,5 @@ import {BaseDriver} from '../driver';
* @param mixin Mixin implementation
*/
export function mixin<C extends Constraints, T extends Partial<BaseDriver<C>>>(mixin: T): void {
// eslint-disable-next-line no-restricted-syntax
Object.assign(BaseDriver.prototype, mixin);
}

View File

@@ -99,7 +99,7 @@ const TimeoutCommands: ITimeoutCommands = {
},
setImplicitWait<C extends Constraints>(this: BaseDriver<C>, ms: number) {
// eslint-disable-line require-await
this.implicitWaitMs = ms;
this.log.debug(`Set implicit wait to ${ms}ms`);
if (this.managedDrivers && this.managedDrivers.length) {

View File

@@ -1,6 +1,3 @@
/* eslint-disable no-unused-vars */
/* eslint-disable require-await */
import {logger} from '@appium/support';
import type {
AppiumLogger,
@@ -14,7 +11,8 @@ import type {
Protocol,
RouteMatcher,
StringRecord,
BidiMethodDef,
BidiModuleMap,
BiDiResultData,
} from '@appium/types';
import AsyncLock from 'async-lock';
import _ from 'lodash';
@@ -112,6 +110,8 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
doesSupportBidi: boolean;
bidiCommands: BidiModuleMap = BIDI_COMMANDS as BidiModuleMap;
constructor(opts: InitialOpts = <InitialOpts>{}, shouldValidateCaps = true) {
this._log = logger.getLogger(helpers.generateDriverLogPrefix(this as Core<C>));
@@ -428,7 +428,18 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
}
}
async executeBidiCommand(bidiCmd: string, bidiParams: StringRecord): Promise<any> {
updateBidiCommands(cmds: BidiModuleMap): void {
const overlappingKeys = _.intersection(Object.keys(cmds), Object.keys(this.bidiCommands));
if (overlappingKeys.length) {
this.log.warn(`Overwriting existing bidi modules: ${JSON.stringify(overlappingKeys)}. This may not be intended!`);
}
this.bidiCommands = {
...this.bidiCommands,
...cmds,
};
}
async executeBidiCommand(bidiCmd: string, bidiParams: StringRecord): Promise<BiDiResultData> {
const [moduleName, methodName] = bidiCmd.split('.');
// if we don't get a valid format for bidi command name, reject
@@ -440,12 +451,13 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
);
}
// if the command module isn't part of our spec, reject
if (!BIDI_COMMANDS[moduleName]) {
// if the command module or method isn't part of our spec, reject
if (!this.bidiCommands[moduleName] || !this.bidiCommands[moduleName][methodName]) {
throw new errors.UnknownCommandError();
}
const {command, params} = BIDI_COMMANDS[moduleName][methodName] as BidiMethodDef;
const {command, params} = this.bidiCommands[moduleName][methodName];
// if the command method isn't part of our spec, also reject
if (!command) {
throw new errors.UnknownCommandError();
@@ -479,8 +491,12 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
`Executing bidi command '${bidiCmd}' with params ${logParams} by passing to driver ` +
`method '${command}'`,
);
const res = (await this[command](...args)) ?? null;
this.log.debug(`Responding to bidi command '${bidiCmd}' with ${JSON.stringify(res)}`);
return res;
const response = await this[command](...args);
const finalResponse = _.isUndefined(response) ? {} : response;
this.log.debug(
`Responding to bidi command '${bidiCmd}' with ` +
`${_.truncate(JSON.stringify(finalResponse), {length: MAX_LOG_BODY_LENGTH})}`
);
return finalResponse;
}
}

View File

@@ -60,7 +60,7 @@ async function createServer (app, cliArgs) {
}
const [cert, key] = await B.all(certKey.map((p) => fs.readFile(p, 'utf8')));
log.debug('Enabling TLS/SPDY on the server using the provided certificate');
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require('spdy').createServer({
cert,
key,

View File

@@ -1,4 +1,4 @@
/* eslint-disable require-await */
import _ from 'lodash';
import B from 'bluebird';

View File

@@ -13,6 +13,10 @@ const BIDI_COMMANDS = /** @type {const} */ ({
command: 'bidiUnsubscribe',
params: SUBSCRIPTION_REQUEST_PARAMS,
},
status: {
command: 'bidiStatus',
params: {},
}
},
browsingContext: {
navigate: {

View File

@@ -60,16 +60,14 @@ export class ProtocolError extends BaseError {
* Get the Bidi protocol version of an error
* @param {string|number} id - the id used in the request for which this error forms the response
* @see https://w3c.github.io/webdriver-bidi/#protocol-definition
* @returns The object conforming to the shape of a BiDi error response
* @returns {import('@appium/types').ErrorBiDiCommandResponse} The object conforming to the shape of a BiDi error response
*/
bidiErrObject(id) {
// if we don't have an id, the client didn't send one, so we have nothing to send back.
// send back an empty string rather than making something up
if (_.isNil(id)) {
id = '';
}
// send back zero rather than making something up
const intId = /** @type {number} */ (_.isInteger(id) ? id : (parseInt(`${id}`, 10) || 0));
return {
id,
id: intId,
type: 'error',
error: this.error,
stacktrace: this.stacktrace,

View File

@@ -67,7 +67,7 @@
"path-to-regexp": "8.2.0",
"serve-favicon": "2.5.0",
"source-map-support": "0.5.21",
"type-fest": "4.30.0",
"type-fest": "4.31.0",
"validate.js": "0.13.1"
},
"optionalDependencies": {

View File

@@ -192,7 +192,7 @@ describe('server plugins', function () {
});
function updaterWithGetRoute(route, reply) {
// eslint-disable-next-line require-await
return async (app, httpServer) => {
app.get(`/${route}`, (req, res) => {
res.header['content-type'] = 'text/html';

View File

@@ -1,4 +1,4 @@
/* eslint-disable require-await */
import {errors, BaseDriver, determineProtocol} from '../../../lib';
import {PROTOCOLS} from '../../../lib/constants';
import {util} from '@appium/support';

View File

@@ -917,7 +917,7 @@ describe('Protocol', function () {
});
it('should pass on any errors in proxying', async function () {
// eslint-disable-next-line require-await
driver.proxyReqRes = async function () {
throw new Error('foo');
};
@@ -938,7 +938,7 @@ describe('Protocol', function () {
});
it('should able to throw ProxyRequestError in proxying', async function () {
// eslint-disable-next-line require-await
driver.proxyReqRes = async function () {
let jsonwp = {
status: 35,
@@ -961,7 +961,7 @@ describe('Protocol', function () {
});
it('should let the proxy handle req/res', async function () {
// eslint-disable-next-line require-await
driver.proxyReqRes = async function (req, res) {
res.status(200).json({custom: 'data'});
};

View File

@@ -1,7 +1,7 @@
// @ts-check
import {createSandbox} from 'sinon';
import _ from 'lodash';
import BaseDriver from '../../../../lib';
import {BaseDriver} from '../../../../lib';
const FIRST_LOGS = ['first', 'logs'];
const SECOND_LOGS = ['second', 'logs'];

View File

@@ -1,4 +1,4 @@
import BaseDriver from '../../../lib';
import {BaseDriver} from '../../../lib';
import {driverUnitTestSuite} from '@appium/driver-test-support';
driverUnitTestSuite(BaseDriver, {

View File

@@ -1,6 +1,6 @@
// @ts-check
import BaseDriver from '../../../lib';
import {BaseDriver} from '../../../lib';
import {createSandbox} from 'sinon';
describe('timeout', function () {

View File

@@ -31,6 +31,8 @@ describe('server configuration', function () {
const chaiAsPromised = await import('chai-as-promised');
chai.use(chaiAsPromised.default);
should = chai.should();
port = await getTestPort(true);
});
function fakeApp() {
@@ -50,10 +52,6 @@ describe('server configuration', function () {
return app;
}
before(async function () {
port = await getTestPort(true);
});
beforeEach(function () {
sandbox = createSandbox();
});

View File

@@ -1,5 +1,3 @@
// transpile:mocha
import {welcome} from '../../../lib/express/static';
import {createSandbox} from 'sinon';

View File

@@ -20,7 +20,7 @@ function resFixture(url, method) {
throw new Error("Can't handle url " + url);
}
// eslint-disable-next-line require-await
async function request(opts) {
const {url, method, json} = opts;
if (/badurl$/.test(url)) {

View File

@@ -32,6 +32,7 @@ describe('proxy', function () {
const chaiAsPromised = await import('chai-as-promised');
chai.use(chaiAsPromised.default);
should = chai.should();
port = await getTestPort();
});
function mockProxy(opts = {}) {
@@ -44,10 +45,6 @@ describe('proxy', function () {
return proxy;
}
before(async function () {
port = await getTestPort();
});
it('should override default params', function () {
let j = mockProxy({server: '127.0.0.2', port});
j.server.should.equal('127.0.0.2');

View File

@@ -1,6 +1,4 @@
// transpile:mocha
import {_} from 'lodash';
import _ from 'lodash';
import {METHOD_MAP, routeToCommandName} from '../../../lib/protocol';
import crypto from 'crypto';
@@ -53,11 +51,6 @@ describe('Protocol', function () {
cmdName.should.equal('timeouts');
});
it('should properly lookup correct command name for endpoint with session', function () {
const cmdName = routeToCommandName('/timeouts/implicit_wait', 'POST');
cmdName.should.equal('implicitWait');
});
it('should properly lookup correct command name for endpoint without session', function () {
const cmdName = routeToCommandName('/status', 'GET');
cmdName.should.equal('getStatus');

View File

@@ -9,7 +9,7 @@ const {getLogger} = require('../build/lib/logger');
const log = getLogger('cli');
// eslint-disable-next-line promise/prefer-await-to-then, promise/prefer-await-to-callbacks
// eslint-disable-next-line promise/prefer-await-to-callbacks
main().catch((err) => {
log.error('Caught otherwise-unhandled rejection (this is probably a bug):', err);
});

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env node
/* eslint-disable no-console */
/**
* Main CLI entry point for `@appium/docutils`

View File

@@ -2,7 +2,7 @@
* Constants used across various modules in this package
* @module
*/
// eslint-disable-next-line @typescript-eslint/no-var-requires -- Consola 3 import call is ESM
const {LogLevels} = require('consola');
import {readFileSync} from 'node:fs';
import {fs} from '@appium/support';

View File

@@ -6,7 +6,7 @@
* @module
*/
// eslint-disable-next-line @typescript-eslint/no-var-requires -- Consola 3 import call is ESM
const {ConsolaInstance, createConsola, LogLevel} = require('consola');
import _ from 'lodash';
import {DEFAULT_LOG_LEVEL, LogLevelMap} from './constants';

View File

@@ -53,7 +53,7 @@
"@sliphua/lilconfig-ts-loader": "3.2.2",
"@types/which": "3.0.4",
"chalk": "4.1.2",
"consola": "3.2.3",
"consola": "3.3.3",
"diff": "7.0.0",
"json5": "2.2.3",
"lilconfig": "3.1.3",
@@ -63,7 +63,7 @@
"semver": "7.6.3",
"source-map-support": "0.5.21",
"teen_process": "2.2.2",
"type-fest": "4.30.0",
"type-fest": "4.31.0",
"typescript": "5.7.2",
"yaml": "2.6.1",
"yargs": "17.7.2",

View File

@@ -1,5 +1,5 @@
mkdocs==1.6.1
mkdocs-git-revision-date-localized-plugin==1.3.0
mkdocs-material==9.5.47
mkdocs-material==9.5.49
mkdocs-redirects==1.2.2
mike==2.1.3

View File

@@ -266,7 +266,7 @@ export function driverUnitTestSuite(
}
}
let results = /** @type {PromiseFulfilledResult<any>[]} */ (
// eslint-disable-next-line promise/no-native
await Promise.allSettled(cmds)
);
for (let i = 1; i < 5; i++) {

View File

@@ -51,7 +51,7 @@
"lodash": "4.17.21",
"sinon": "19.0.2",
"source-map-support": "0.5.21",
"type-fest": "4.30.0"
"type-fest": "4.31.0"
},
"peerDependencies": {
"appium": "^2.0.0-beta.43 || ^3.0.0-beta.0",

View File

@@ -1,120 +0,0 @@
/**
* `@appium/eslint-config-appium-ts` is a configuration for ESLint which extends
* `@appium/eslint-config-appium` and adds TypeScript support.
*
* It is **not** a _replacement for_ `@appium/eslint-config-appium`.
*
* It can be used _without any `.ts` sources_, as long as a `tsconfig.json` exists in the project
* root. In that case, it will run on `.js` files which are enabled for checking; this includes the
* `checkJs` setting and any `// @ts-check` directive in source files.
*/
module.exports = {
$schema: 'http://json.schemastore.org/eslintrc',
parser: '@typescript-eslint/parser',
extends: ['@appium/eslint-config-appium', 'plugin:@typescript-eslint/recommended'],
rules: {
/**
* This rule is configured to warn if a `@ts-ignore` or `@ts-expect-error` directive is used
* without explanation.
* @remarks It's good practice to explain why things break!
*/
'@typescript-eslint/ban-ts-comment': [
'warn',
{
'ts-expect-error': 'allow-with-description',
'ts-ignore': 'allow-with-description',
},
],
/**
* Empty functions are allowed.
* @remarks This is disabled because I need someone to explain to me why empty functions are bad. I suppose they _could_ be bugs, but so could literally any line of code.
*/
'@typescript-eslint/no-empty-function': 'off',
/**
* Empty interfaces are allowed.
* @remarks This is because empty interfaces have a use case in declaration merging. Otherwise,
* an empty interface can be a type alias, e.g., `type Foo = Bar` where `Bar` is an interface.
*/
'@typescript-eslint/no-empty-interface': 'off',
/**
* Explicit `any` types are allowed.
* @remarks Eventually this should be a warning, and finally an error, as we fully type the codebases.
*/
'@typescript-eslint/no-explicit-any': 'off',
/**
* Warns if a non-null assertion (`!`) is used.
* @remarks Generally, a non-null assertion should be replaced by a proper type guard or
* type-safe function, if possible. For example, `Set.prototype.has(x)` is not type-safe, and
* does not imply that `Set.prototype.get(x)` is not `undefined` (I do not know why this is, but
* I'm sure there's a good reason for it). In this case, a non-null assertion is appropriate.
* Often a simple `typeof x === 'y'` conditional is sufficient to narrow the type and avoid the
* non-null assertion.
*/
'@typescript-eslint/no-non-null-assertion': 'warn',
/**
* This disallows use of `require()`.
* @remarks We _do_ use `require()` fairly often to load files on-the-fly; however, these may
* want to be replaced with `import()` (I am not sure if there's a rule about that?). **If this check fails**, disable the rule for the particular line.
*/
'@typescript-eslint/no-var-requires': 'error',
/**
* Sometimes we want unused variables to be present in base class method declarations.
*/
'@typescript-eslint/no-unused-vars': 'warn',
/**
* Allow native `Promise`s. **This overrides `@appium/eslint-config-appium`.**
* @remarks Originally, this was so that we could use [bluebird](https://npm.im/bluebird)
* everywhere, but this is not strictly necessary.
*/
'promise/no-native': 'off',
/**
* Allow `async` functions without `await`. **This overrides `@appium/eslint-config-appium`.**
* @remarks Originally, this was to be more clear about the return value of a function, but with
* the addition of types, this is no longer necessary. Further, both `return somePromise` and
* `return await somePromise` have their own use-cases.
*/
'require-await': 'off',
/**
* Disables the `brace-style` rule.
* @remarks Due to the way `prettier` sometimes formats extremely verbose types, sometimes it is necessary
* to indent in a way that is not allowed by the default `brace-style` rule.
*/
'brace-style': 'off',
},
/**
* This stuff enables `eslint-plugin-import` to resolve TS modules.
*/
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {
project: ['tsconfig.json', './packages/*/tsconfig.json'],
},
},
},
overrides: [
/**
* Overrides for tests.
*/
{
files: ['**/test/**', '*.spec.js', '-specs.js', '*.spec.ts'],
rules: {
/**
* Both `@ts-expect-error` and `@ts-ignore` are allowed to be used with impunity in tests.
* @remarks We often test things which explicitly violate types.
*/
'@typescript-eslint/ban-ts-comment': 'off',
/**
* Allow non-null assertions in tests; do not even warn.
* @remarks The idea is that the assertions themselves will be written in such a way that if
* the non-null assertion was invalid, the assertion would fail.
*/
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
],
};

View File

@@ -0,0 +1,231 @@
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import fs from 'node:fs';
import js from '@eslint/js';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import globals from 'globals';
import pluginPromise from 'eslint-plugin-promise';
import importPlugin from 'eslint-plugin-import';
import mochaPlugin from 'eslint-plugin-mocha';
import {includeIgnoreFile} from '@eslint/compat';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const gitignorePath = path.resolve(__dirname, '.gitignore');
export default [
js.configs.recommended,
pluginPromise.configs['flat/recommended'],
importPlugin.flatConfigs.recommended,
{
name: 'Script Files',
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
parser: tsParser,
globals: {
...globals.node,
NodeJS: 'readonly',
BufferEncoding: 'readonly',
},
},
plugins: {
'@typescript-eslint': tsPlugin,
},
settings: {
/**
* This stuff enables `eslint-plugin-import` to resolve TS modules.
*/
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx', '.mtsx'],
},
'import/resolver': {
typescript: {
project: ['tsconfig.json', './packages/*/tsconfig.json'],
},
},
},
rules: {
...tsPlugin.configs.recommended.rules,
'no-console': 2,
semi: [2, 'always'],
radix: [2, 'always'],
'dot-notation': 2,
eqeqeq: [2, 'smart'],
'comma-dangle': 0,
'no-empty': 0,
'object-shorthand': 2,
'arrow-parens': [1, 'always'],
'arrow-body-style': [1, 'as-needed'],
'import/export': 2,
'import/no-unresolved': 2,
'import/no-duplicates': 2,
'promise/no-return-wrap': 1,
'promise/param-names': 1,
'promise/catch-or-return': 1,
'promise/prefer-await-to-then': 1,
'promise/prefer-await-to-callbacks': 1,
'no-var': 2,
curly: [2, 'all'],
// enforce spacing
'arrow-spacing': 2,
'keyword-spacing': 2,
'comma-spacing': [
2,
{
before: false,
after: true,
},
],
'array-bracket-spacing': 2,
'no-trailing-spaces': 2,
'no-whitespace-before-property': 2,
'space-in-parens': [2, 'never'],
'space-before-blocks': [2, 'always'],
'space-unary-ops': [
2,
{
words: true,
nonwords: false,
},
],
'space-infix-ops': 2,
'key-spacing': [
2,
{
mode: 'strict',
beforeColon: false,
afterColon: true,
},
],
'no-multi-spaces': 2,
quotes: [
2,
'single',
{
avoidEscape: true,
allowTemplateLiterals: true,
},
],
'no-buffer-constructor': 1,
'require-atomic-updates': 0,
'no-prototype-builtins': 1,
'no-redeclare': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'no-dupe-class-members': 'off',
'@typescript-eslint/no-var-requires': 'off',
'import/named': 'warn',
/**
* This rule is configured to warn if a `@ts-ignore` or `@ts-expect-error` directive is used
* without explanation.
* @remarks It's good practice to explain why things break!
*/
'@typescript-eslint/ban-ts-comment': [
'warn',
{
'ts-expect-error': 'allow-with-description',
'ts-ignore': 'allow-with-description',
},
],
/**
* Empty functions are allowed.
* @remarks This is disabled because I need someone to explain to me why empty functions are bad. I suppose they _could_ be bugs, but so could literally any line of code.
*/
'@typescript-eslint/no-empty-function': 'off',
/**
* Empty interfaces are allowed.
* @remarks This is because empty interfaces have a use case in declaration merging. Otherwise,
* an empty interface can be a type alias, e.g., `type Foo = Bar` where `Bar` is an interface.
*/
'@typescript-eslint/no-empty-interface': 'off',
/**
* Explicit `any` types are allowed.
* @remarks Eventually this should be a warning, and finally an error, as we fully type the codebases.
*/
'@typescript-eslint/no-explicit-any': 'off',
/**
* Warns if a non-null assertion (`!`) is used.
* @remarks Generally, a non-null assertion should be replaced by a proper type guard or
* type-safe function, if possible. For example, `Set.prototype.has(x)` is not type-safe, and
* does not imply that `Set.prototype.get(x)` is not `undefined` (I do not know why this is, but
* I'm sure there's a good reason for it). In this case, a non-null assertion is appropriate.
* Often a simple `typeof x === 'y'` conditional is sufficient to narrow the type and avoid the
* non-null assertion.
*/
'@typescript-eslint/no-non-null-assertion': 'warn',
/**
* Sometimes we want unused variables to be present in base class method declarations.
*/
'@typescript-eslint/no-unused-vars': 'warn',
/**
* Allow native `Promise`s. **This overrides `@appium/eslint-config-appium`.**
* @remarks Originally, this was so that we could use [bluebird](https://npm.im/bluebird)
* everywhere, but this is not strictly necessary.
*/
'promise/no-native': 'off',
/**
* Allow `async` functions without `await`. **This overrides `@appium/eslint-config-appium`.**
* @remarks Originally, this was to be more clear about the return value of a function, but with
* the addition of types, this is no longer necessary. Further, both `return somePromise` and
* `return await somePromise` have their own use-cases.
*/
'require-await': 'off',
/**
* Disables the `brace-style` rule.
* @remarks Due to the way `prettier` sometimes formats extremely verbose types, sometimes it is necessary
* to indent in a way that is not allowed by the default `brace-style` rule.
*/
'brace-style': 'off',
}
},
{
...mochaPlugin.configs.flat.recommended,
name: 'Test Files',
files: ['**/test/**', '*.spec.*js', '-specs.*js', '*.spec.ts'],
rules: {
...mochaPlugin.configs.flat.recommended.rules,
/**
* Both `@ts-expect-error` and `@ts-ignore` are allowed to be used with impunity in tests.
* @remarks We often test things which explicitly violate types.
*/
'@typescript-eslint/ban-ts-comment': 'off',
/**
* Allow non-null assertions in tests; do not even warn.
* @remarks The idea is that the assertions themselves will be written in such a way that if
* the non-null assertion was invalid, the assertion would fail.
*/
'@typescript-eslint/no-non-null-assertion': 'off',
'mocha/no-exclusive-tests': 2,
'mocha/no-mocha-arrows': 2,
'mocha/max-top-level-suites': 'off',
'mocha/consistent-spacing-between-blocks': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'mocha/no-setup-in-describe': 'off',
'mocha/no-exports': 'off',
'import/no-named-as-default-member': 'off',
'mocha/no-skipped-tests': 'off',
},
},
{
name: 'Ignores',
ignores: [
...(fs.existsSync(gitignorePath) ? includeIgnoreFile(gitignorePath).ignores : []),
'**/*-d.ts',
'**/build/**',
'**/*.min.js',
'**/coverage/**',
'**/.*',
],
}
];

View File

@@ -1,6 +1,7 @@
{
"name": "@appium/eslint-config-appium-ts",
"version": "0.3.3",
"type": "module",
"version": "1.0.0",
"description": "Shared ESLint config for Appium projects (TypeScript version)",
"keywords": [
"eslint",
@@ -20,23 +21,25 @@
},
"license": "Apache-2.0",
"author": "https://github.com/appium",
"main": "index.js",
"exports": "./index.mjs",
"files": [
"index.js"
"index.mjs"
],
"scripts": {
"test:smoke": "exit 0"
},
"peerDependencies": {
"@appium/eslint-config-appium": "^8.0.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.5.0",
"eslint-plugin-import": "^2.26.0",
"@eslint/compat": "^1.2.4",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.10.0",
"@typescript-eslint/eslint-plugin": "^8.18.2",
"@typescript-eslint/parser": "^8.18.2",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.7.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-mocha": "^10.1.0",
"eslint-plugin-promise": "^6.0.0"
"eslint-plugin-promise": "^7.2.0"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",

View File

@@ -1,184 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.0.5](https://github.com/appium/appium/compare/@appium/eslint-config-appium@8.0.4...@appium/eslint-config-appium@8.0.5) (2023-10-18)
**Note:** Version bump only for package @appium/eslint-config-appium
## [8.0.4](https://github.com/appium/appium/compare/@appium/eslint-config-appium@8.0.3...@appium/eslint-config-appium@8.0.4) (2023-07-03)
### Bug Fixes
* **eslint-config-appium:** remove prototype assignment warning ([679865e](https://github.com/appium/appium/commit/679865ed2f1df59baef8c4d884b0a63a9222940e))
## [8.0.3](https://github.com/appium/appium/compare/@appium/eslint-config-appium@8.0.2...@appium/eslint-config-appium@8.0.3) (2023-04-03)
**Note:** Version bump only for package @appium/eslint-config-appium
## [8.0.2](https://github.com/appium/appium/compare/@appium/eslint-config-appium@8.0.1...@appium/eslint-config-appium@8.0.2) (2023-03-28)
### Bug Fixes
* **eslint-config-appium:** disable import/no-unresolved for tsd tests ([020473a](https://github.com/appium/appium/commit/020473afc43e68215a576cb3d723b2de2de8e8ad))
## [8.0.1](https://github.com/appium/appium/compare/@appium/eslint-config-appium@8.0.0...@appium/eslint-config-appium@8.0.1) (2023-03-08)
**Note:** Version bump only for package @appium/eslint-config-appium
# [8.0.0](https://github.com/appium/appium/compare/@appium/eslint-config-appium@7.0.0...@appium/eslint-config-appium@8.0.0) (2022-12-14)
- chore!: set engines to minimum Node.js v14.17.0 ([a1dbe6c](https://github.com/appium/appium/commit/a1dbe6c43efe76604943a607d402f4c8b864d652))
### BREAKING CHANGES
- Appium now supports version range `^14.17.0 || ^16.13.0 || >=18.0.0`
# [7.0.0](https://github.com/appium/appium/compare/@appium/eslint-config-appium@6.0.4...@appium/eslint-config-appium@7.0.0) (2022-09-07)
### chore
- **eslint-config-appium:** upgrade to ESLint v8 ([887cb44](https://github.com/appium/appium/commit/887cb449e61be585c084af2f6422d2e02be5028b))
### BREAKING CHANGES
- **eslint-config-appium:** This change upgrades to ESLint v8 and removes `@babel/eslint-parser`, which is only necessary if we're using mid-stage TC39 proposals (which we aren't). I think `@babel/core` was a peer dep of `@babel/eslint-parser`, so I removed that too. And removed cruft from the main configuration.
ESLint rules _very likely_ had breaking changes, but I didn't experience any on our codebase. However, this version of ESLint seems to be incompatible with `gulp-eslint`, so `@appium/gulp-plugins` should be held back from upgrading.
In addition:
- Updated `README.md`
- Updated some fields in `package.json`
- Loosened `peerDependencies`, as they're supposed to be loose.
## [6.0.4](https://github.com/appium/appium/compare/@appium/eslint-config-appium@6.0.3...@appium/eslint-config-appium@6.0.4) (2022-08-03)
### Bug Fixes
- **appium,base-driver,base-plugin,doctor,docutils,eslint-config-appium,execute-driver-plugin,fake-driver,fake-plugin,gulp-plugins,images-plugin,opencv,relaxed-caps-plugin,schema,support,test-support,types,universal-xml-plugin:** update engines ([d8d2382](https://github.com/appium/appium/commit/d8d2382327ba7b7db8a4d1cad987c0e60184c92d))
## [6.0.3](https://github.com/appium/appium/compare/@appium/eslint-config-appium@6.0.2...@appium/eslint-config-appium@6.0.3) (2022-07-28)
**Note:** Version bump only for package @appium/eslint-config-appium
## [6.0.2](https://github.com/appium/appium/compare/@appium/eslint-config-appium@6.0.1...@appium/eslint-config-appium@6.0.2) (2022-05-31)
**Note:** Version bump only for package @appium/eslint-config-appium
## [6.0.1](https://github.com/appium/appium/compare/@appium/eslint-config-appium@6.0.0...@appium/eslint-config-appium@6.0.1) (2022-05-31)
**Note:** Version bump only for package @appium/eslint-config-appium
# [6.0.0](https://github.com/appium/appium/compare/@appium/eslint-config-appium@5.1.1...@appium/eslint-config-appium@6.0.0) (2022-05-03)
### Features
- **eslint-config-appium,gulp-plugins:** add prettier ([878bb6a](https://github.com/appium/appium/commit/878bb6a44f85fd43e0f3678b95cddb8d7cbba69a))
### BREAKING CHANGES
- **eslint-config-appium,gulp-plugins:** `@appium/eslint-config-appium` now requires peer dependency `eslint-config-prettier`. Because `@appium/gulp-plugins` always uses the latest development version of `@appium/eslint-config-appium`, the dependency needs to be added there, too.
In addition, this disables some rules, so _may_ cause code which previously passed lint checks _not_ to pass lint checks.
## [5.1.1](https://github.com/appium/appium/compare/@appium/eslint-config-appium@5.1.0...@appium/eslint-config-appium@5.1.1) (2022-05-03)
### Bug Fixes
- **eslint-config-appium:** remove custom indenting rules ([d89203f](https://github.com/appium/appium/commit/d89203f96c7d45e8cda5e447c808d1485449c284))
- **eslint-config-appium:** revert prettier-related change ([93e05a8](https://github.com/appium/appium/commit/93e05a82696514be04d9792c90eb3fe7e3fa0143))
# [5.1.0](https://github.com/appium/appium/compare/@appium/eslint-config-appium@5.0.6...@appium/eslint-config-appium@5.1.0) (2022-05-02)
### Bug Fixes
- **eslint-config-appium:** disable space-before-function-paren ([88a6655](https://github.com/appium/appium/commit/88a6655253a4879041478d64254471efebe4cbfe))
### Features
- **eslint-config-appium:** adds a warning if code attempts to assign to an object prototype ([5bdc476](https://github.com/appium/appium/commit/5bdc476c626caa301c7cb4ffc01c296f437deb06)), closes [#16829](https://github.com/appium/appium/issues/16829)
## [5.0.6](https://github.com/appium/appium/compare/@appium/eslint-config-appium@5.0.5...@appium/eslint-config-appium@5.0.6) (2022-04-20)
**Note:** Version bump only for package @appium/eslint-config-appium
## [5.0.5](https://github.com/appium/appium/compare/@appium/eslint-config-appium@5.0.4...@appium/eslint-config-appium@5.0.5) (2022-04-20)
**Note:** Version bump only for package @appium/eslint-config-appium
## [5.0.4](https://github.com/appium/appium/compare/@appium/eslint-config-appium@5.0.3...@appium/eslint-config-appium@5.0.4) (2022-04-12)
**Note:** Version bump only for package @appium/eslint-config-appium
## [5.0.3](https://github.com/appium/appium/compare/@appium/eslint-config-appium@5.0.2...@appium/eslint-config-appium@5.0.3) (2022-04-07)
**Note:** Version bump only for package @appium/eslint-config-appium
## [5.0.2](https://github.com/appium/appium/compare/@appium/eslint-config-appium@5.0.1...@appium/eslint-config-appium@5.0.2) (2022-03-22)
**Note:** Version bump only for package @appium/eslint-config-appium
## [5.0.1](https://github.com/appium/appium/compare/@appium/eslint-config-appium@5.0.0...@appium/eslint-config-appium@5.0.1) (2022-01-11)
**Note:** Version bump only for package @appium/eslint-config-appium
# [5.0.0](https://github.com/appium/appium/compare/@appium/eslint-config-appium@4.7.4...@appium/eslint-config-appium@5.0.0) (2021-11-19)
### Bug Fixes
- **eslint-config-appium:** switch to peerdeps ([7fb1667](https://github.com/appium/appium/commit/7fb1667a3b702a22ec365b6fc8e88c88e4e24573))
### BREAKING CHANGES
- **eslint-config-appium:** ESLint expects configs or plugins which require other configs or plugins to have _peer dependencies_ of those things _and_ of ESLint itself. All deps have been changed to peer deps, and this module now requires the installation of the following _if_ using npm older than v7:
```
"@babel/core": "7.16.0",
"@babel/eslint-parser": "7.16.3",
"eslint": "7.32.0",
"eslint-plugin-import": "2.25.3",
"eslint-plugin-mocha": "9.0.0",
"eslint-plugin-promise": "5.1.1"
```
npm@7 will install these automatically if they do not exist.
## [4.7.4](https://github.com/appium/appium/compare/@appium/eslint-config-appium@4.7.3...@appium/eslint-config-appium@4.7.4) (2021-11-15)
**Note:** Version bump only for package @appium/eslint-config-appium
## [4.7.3](https://github.com/appium/appium/compare/@appium/eslint-config-appium@4.7.2...@appium/eslint-config-appium@4.7.3) (2021-11-09)
**Note:** Version bump only for package @appium/eslint-config-appium
## [4.7.2](https://github.com/appium/appium/compare/@appium/eslint-config-appium@4.7.1...@appium/eslint-config-appium@4.7.2) (2021-09-14)
**Note:** Version bump only for package @appium/eslint-config-appium
## [4.7.1](https://github.com/appium/appium/compare/@appium/eslint-config-appium@4.7.0...@appium/eslint-config-appium@4.7.1) (2021-08-16)
# 2.0.0-beta (2021-08-13)
**Note:** Version bump only for package @appium/eslint-config-appium

View File

@@ -1,41 +0,0 @@
# @appium/eslint-config-appium
> Provides a reusable [ESLint](http://eslint.org/) [shared configuration](http://eslint.org/docs/developer-guide/shareable-configs) for [Appium](https://github.com/appium/appium) and Appium-adjacent projects.
[![NPM version](http://img.shields.io/npm/v/@appium/eslint-config-appium.svg)](https://npmjs.org/package/@appium/eslint-config-appium)
[![Downloads](http://img.shields.io/npm/dm/@appium/eslint-config-appium.svg)](https://npmjs.org/package/@appium/eslint-config-appium)
## Usage
Install the package with **`npm` v8 or newer**:
```bash
npm install @appium/eslint-config-appium --save-dev
```
And then, in your `.eslintrc` file, extend the configuration:
```json
{
"extends": "@appium/eslint-config-appium"
}
```
## Peer Dependencies
This config requires the following packages be installed (as peer dependencies) in your project. See the `package.json` for the required versions.
- [eslint](https://www.npmjs.com/package/eslint)
- [eslint-config-prettier](https://www.npmjs.com/package/eslint-config-prettier)
- [eslint-plugin-import](https://www.npmjs.com/package/eslint-plugin-import)
- [eslint-plugin-mocha](https://www.npmjs.com/package/eslint-plugin-mocha)
- [eslint-plugin-promise](https://www.npmjs.com/package/eslint-plugin-promise)
## Notes
- This configuration is intended to be used alongside [Prettier](https://www.npmjs.com/package/prettier).
- This package was previously published as `eslint-config-appium`.
## License
Copyright © 2016 OpenJS Foundation. Licensed Apache-2.0

View File

@@ -1,102 +0,0 @@
module.exports = {
extends: ['eslint:recommended', 'prettier'],
parserOptions: {
requireConfigFile: false,
sourceType: 'module',
},
env: {
node: true,
mocha: true,
es2022: true,
},
plugins: ['import', 'mocha', 'promise'],
globals: {
chai: true,
should: true,
},
rules: {
'no-console': 2,
semi: [2, 'always'],
radix: [2, 'always'],
'dot-notation': 2,
eqeqeq: [2, 'smart'],
'brace-style': [
2,
'1tbs',
{
allowSingleLine: true,
},
],
'comma-dangle': 0,
'no-empty': 0,
'object-shorthand': 2,
'arrow-parens': [1, 'always'],
'arrow-body-style': [1, 'as-needed'],
'import/export': 2,
'import/no-unresolved': 2,
'import/no-duplicates': 2,
'mocha/no-exclusive-tests': 2,
'mocha/no-mocha-arrows': 2,
'promise/no-return-wrap': 1,
'promise/param-names': 1,
'promise/catch-or-return': 1,
'promise/no-native': 2,
'promise/prefer-await-to-then': 1,
'promise/prefer-await-to-callbacks': 1,
'require-await': 2,
'no-var': 2,
curly: [2, 'all'],
// enforce spacing
'arrow-spacing': 2,
'keyword-spacing': 2,
'comma-spacing': [
2,
{
before: false,
after: true,
},
],
'array-bracket-spacing': 2,
'no-trailing-spaces': 2,
'no-whitespace-before-property': 2,
'space-in-parens': [2, 'never'],
'space-before-blocks': [2, 'always'],
'space-unary-ops': [
2,
{
words: true,
nonwords: false,
},
],
'space-infix-ops': 2,
'key-spacing': [
2,
{
mode: 'strict',
beforeColon: false,
afterColon: true,
},
],
'no-multi-spaces': 2,
quotes: [
2,
'single',
{
avoidEscape: true,
allowTemplateLiterals: true,
},
],
'no-buffer-constructor': 1,
'require-atomic-updates': 0,
'no-prototype-builtins': 1,
'no-redeclare': 1,
},
overrides: [
/**
* This disables the `import` plugin from trying to resolve `.test-d.ts` files,
* which have a weird resolution strategy.
*/
{files: '*.test-d.ts', rules: {'import/no-unresolved': 'off'}},
],
};

View File

@@ -1,47 +0,0 @@
{
"name": "@appium/eslint-config-appium",
"version": "8.0.5",
"description": "Shared ESLint config for Appium projects",
"keywords": [
"eslint",
"eslintconfig",
"appium"
],
"homepage": "https://appium.io",
"bugs": {
"url": "https://github.com/appium/appium/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/appium/appium.git",
"directory": "packages/eslint-config-appium"
},
"license": "Apache-2.0",
"author": "https://github.com/appium",
"main": "index.js",
"files": [
"index.js"
],
"scripts": {
"eslint:find:all-rules": "eslint-find-rules -a",
"eslint:find:current-rules": "eslint-find-rules -c",
"eslint:find:plugin-rules": "eslint-find-rules -p",
"eslint:find:unused-rules": "eslint-find-rules -u -n",
"test:smoke": "node ./index.js"
},
"peerDependencies": {
"eslint": "^8.21.0",
"eslint-config-prettier": "^8.5.0 || ^9.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-mocha": "^10.1.0",
"eslint-plugin-promise": "^6.0.0"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
"npm": ">=8"
},
"publishConfig": {
"access": "public"
},
"gitHead": "8480a85ce2fa466360e0fb1a7f66628331907f02"
}

View File

@@ -42,7 +42,7 @@
"bluebird": "3.7.2",
"lodash": "4.17.21",
"source-map-support": "0.5.21",
"webdriverio": "8.40.6"
"webdriverio": "9.4.5"
},
"peerDependencies": {
"appium": "^2.0.0-beta.35 || ^3.0.0-beta.0"

View File

@@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const {doctor} = require('appium/support');
/** @satisfies {import('@appium/types').IDoctorCheck} */

View File

@@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const {EnvVarAndPathCheck} = require('./common');
const fakeCheck1 = new EnvVarAndPathCheck('FAKE1');

View File

@@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const {EnvVarAndPathCheck} = require('./common');
const fakeCheck2 = new EnvVarAndPathCheck('FAKE2');

View File

@@ -221,6 +221,20 @@ export class FakeDriver extends BaseDriver {
await B.delay(1);
}
static newBidiCommands = /** @type {const} */({
fake: {
getFakeThing: {
command: 'getFakeThing',
},
setFakeThing: {
command: 'setFakeThing',
params: {
required: ['thing'],
},
},
}
});
static newMethodMap = /** @type {const} */ ({
'/session/:sessionId/fakedriver': {
GET: {command: 'getFakeThing'},
@@ -266,7 +280,7 @@ export class FakeDriver extends BaseDriver {
}
static async updateServer(expressApp, httpServer, cliArgs) {
// eslint-disable-line require-await
expressApp.all('/fakedriver', FakeDriver.fakeRoute);
expressApp.all('/fakedriverCliArgs', (req, res) => {
res.send(JSON.stringify(cliArgs));

View File

@@ -1,5 +1,5 @@
// @ts-check
/* eslint-disable no-case-declarations */
import {BasePlugin} from 'appium/plugin';
import B from 'bluebird';
@@ -59,7 +59,7 @@ class FakePlugin extends BasePlugin {
FakePlugin._unexpectedData = null;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars,require-await
static async updateServer(expressApp, httpServer, cliArgs) {
expressApp.all('/fake', FakePlugin.fakeRoute);
expressApp.all('/unexpected', FakePlugin.unexpectedData);
@@ -112,7 +112,7 @@ class FakePlugin extends BasePlugin {
return `<<${handle}>>`;
}
// eslint-disable-next-line require-await
async onUnexpectedShutdown(driver, cause) {
FakePlugin._unexpectedData = `Session ended because ${cause}`;
}

View File

@@ -1,4 +1,4 @@
import FakePlugin from '../../lib/plugin';
import {FakePlugin} from '../../lib/plugin';
import B from 'bluebird';
class FakeExpress {

View File

@@ -1,4 +1,4 @@
/* eslint-disable no-case-declarations */
import _ from 'lodash';
import {errors} from 'appium/driver';

View File

@@ -4,7 +4,7 @@ import {BaseDriver} from 'appium/driver';
import {ImageElementPlugin} from '../../lib/plugin';
import {IMAGE_STRATEGY} from '../../lib/constants';
import ImageElementFinder from '../../lib/finder';
import ImageElement from '../../lib/image-element';
import {ImageElement} from '../../lib/image-element';
import sinon from 'sinon';
import {TINY_PNG, TiNY_PNG_BUF, TINY_PNG_DIMS} from '../fixtures';
import sharp from 'sharp';

View File

@@ -1,9 +1,9 @@
import _ from 'lodash';
import BaseDriver from 'appium/driver';
import {BaseDriver} from 'appium/driver';
import {util} from 'appium/support';
import ImageElementFinder from '../../lib/finder';
import {getImgElFromArgs} from '../../lib/plugin';
import ImageElement from '../../lib/image-element';
import {ImageElement} from '../../lib/image-element';
import sinon from 'sinon';
import {IMAGE_ELEMENT_PREFIX} from '../../lib/constants';

View File

@@ -5,7 +5,7 @@ import {
MATCH_TEMPLATE_MODE,
IMAGE_STRATEGY,
} from '../../lib/constants';
import BaseDriver from 'appium/driver';
import {BaseDriver} from 'appium/driver';
import {TEST_IMG_1_B64, TEST_IMG_2_B64, TEST_IMG_2_PART_B64} from '../fixtures';
import {util} from '@appium/support';

View File

@@ -3,12 +3,15 @@ import { Log } from '../../lib/log';
import { unleakString } from '../../lib/utils';
import {Stream} from 'node:stream';
describe('basic', async function () {
const chai = await import('chai');
chai.should();
describe('basic', function () {
let chai;
let log;
before(async function () {
chai = await import('chai');
chai.should();
});
describe('logging', function () {
let s;
let result = [];
@@ -16,45 +19,45 @@ describe('basic', async function () {
let logInfoEvents = [];
let logPrefixEvents = [];
const resultExpect = [
// eslint-disable-next-line max-len
'\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[7msill\u001b[0m \u001b[0m\u001b[35msilly prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[36;40mverb\u001b[0m \u001b[0m\u001b[35mverbose prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[32minfo\u001b[0m \u001b[0m\u001b[35minfo prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[32;40mtiming\u001b[0m \u001b[0m\u001b[35mtiming prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[32;40mhttp\u001b[0m \u001b[0m\u001b[35mhttp prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[36;40mnotice\u001b[0m \u001b[0m\u001b[35mnotice prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[30;43mWARN\u001b[0m \u001b[0m\u001b[35mwarn prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[31;40mERR!\u001b[0m \u001b[0m\u001b[35merror prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[32minfo\u001b[0m \u001b[0m\u001b[35minfo prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[32;40mtiming\u001b[0m \u001b[0m\u001b[35mtiming prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[32;40mhttp\u001b[0m \u001b[0m\u001b[35mhttp prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[36;40mnotice\u001b[0m \u001b[0m\u001b[35mnotice prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[30;43mWARN\u001b[0m \u001b[0m\u001b[35mwarn prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[31;40mERR!\u001b[0m \u001b[0m\u001b[35merror prefix\u001b[0m x = {"foo":{"bar":"baz"}}\n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[31;40mERR!\u001b[0m \u001b[0m\u001b[35m404\u001b[0m This is a longer\n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[31;40mERR!\u001b[0m \u001b[0m\u001b[35m404\u001b[0m message, with some details\n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[31;40mERR!\u001b[0m \u001b[0m\u001b[35m404\u001b[0m and maybe a stack.\n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u001b[31;40mERR!\u001b[0m \u001b[0m\u001b[35m404\u001b[0m \n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u0007noise\u001b[0m\u001b[35m\u001b[0m LOUD NOISES\n',
// eslint-disable-next-line max-len
'\u001b[0m\u001b[37;40mnpm\u001b[0m \u001b[0m\u0007noise\u001b[0m \u001b[0m\u001b[35merror\u001b[0m erroring\n',
'\u001b[0m',
];
@@ -230,7 +233,7 @@ describe('basic', async function () {
},
];
this.beforeEach(function () {
beforeEach(function () {
result = [];
logEvents = [];
logInfoEvents = [];

View File

@@ -1,7 +1,7 @@
import path from 'node:path';
import rewiremock from 'rewiremock/node';
import type {Strongbox as TStrongbox, StrongboxOpts, Item, Value} from '../../lib';
import {createSandbox, SinonSandbox, SinonStubbedMember} from 'sinon';
import {createSandbox, SinonSandbox, SinonStubbedMember, SinonStub} from 'sinon';
import type fs from 'node:fs/promises';
type MockFs = {
@@ -165,7 +165,7 @@ describe('Strongbox', function () {
});
describe('clearAll()', function () {
let clear: sinon.SinonStub<never[], Promise<void>>;
let clear: SinonStub<never[], Promise<void>>;
beforeEach(async function () {
const item = await box.createItem<string>('SLUG test');

View File

@@ -3,7 +3,7 @@ import _ from 'lodash';
import {homedir} from 'os';
import path from 'path';
import readPkg from 'read-pkg';
import semver from 'semver';
import * as semver from 'semver';
/**
* Path to the default `APPIUM_HOME` dir (`~/.appium`).

View File

@@ -24,7 +24,7 @@ import readPkg from 'read-pkg';
import sanitize from 'sanitize-filename';
import which from 'which';
import log from './logger';
import Timer from './timing';
import {Timer} from './timing';
import {isWindows} from './system';
import {pluralize} from './util';
@@ -239,6 +239,7 @@ const fs = {
let directoryCount = 0;
const timer = new Timer().start();
return await new B(function (resolve, reject) {
/** @type {Promise} */
let lastFileProcessed = B.resolve();
walker = klaw(dir, {
depthLimit: recursive ? -1 : 0,
@@ -253,16 +254,18 @@ const fs = {
directoryCount++;
}
// eslint-disable-next-line promise/prefer-await-to-callbacks
lastFileProcessed = B.try(async () => await callback(item.path, item.stats.isDirectory()))
.then(function (done = false) {
lastFileProcessed = (async () => {
try {
// eslint-disable-next-line promise/prefer-await-to-callbacks
const done = await callback(item.path, item.stats.isDirectory());
if (done) {
resolve(item.path);
} else {
walker.resume();
return resolve(item.path);
}
})
.catch(reject);
walker.resume();
} catch (err) {
return reject(err);
}
})();
})
.on('error', function (err, item) {
log.warn(`Got an error while walking '${item.path}': ${err.message}`);
@@ -273,14 +276,15 @@ const fs = {
}
})
.on('end', function () {
lastFileProcessed
.then((file) => {
resolve(/** @type {string|undefined} */ (file) ?? null);
})
.catch(function (err) {
(async () => {
try {
const file = await lastFileProcessed;
return resolve(/** @type {string|undefined} */ (file) ?? null);
} catch (err) {
log.warn(`Unexpected error: ${err.message}`);
reject(err);
});
return reject(err);
}
})();
});
}).finally(function () {
log.debug(

View File

@@ -1,7 +1,7 @@
// @ts-check
import path from 'path';
import semver from 'semver';
import * as semver from 'semver';
import {hasAppiumDependency} from './env';
import {exec} from 'teen_process';
import {fs} from './fs';

View File

@@ -123,7 +123,7 @@ const openDir = tempDir;
*
* @returns {Promise<string>} A temp directory path whcih is defined as static in the same process
*/
// eslint-disable-next-line require-await
async function staticDir() {
return _static;
}

View File

@@ -2,8 +2,8 @@ import B from 'bluebird';
import _ from 'lodash';
import os from 'os';
import path from 'path';
import fs from './fs';
import semver from 'semver';
import { fs } from './fs';
import * as semver from 'semver';
import {
// https://www.npmjs.com/package/shell-quote
quote as shellQuote,
@@ -19,7 +19,7 @@ import {
v4 as uuidV4,
v5 as uuidV5,
} from 'uuid';
import _lockfile from 'lockfile';
import * as _lockfile from 'lockfile';
const W3C_WEB_ELEMENT_IDENTIFIER = 'element-6066-11e4-a52e-4f735466cecf';
const KiB = 1024;

View File

@@ -89,7 +89,7 @@
"source-map-support": "0.5.21",
"supports-color": "8.1.1",
"teen_process": "2.2.2",
"type-fest": "4.30.0",
"type-fest": "4.31.0",
"uuid": "11.0.3",
"which": "4.0.0",
"yauzl": "3.2.0"

View File

@@ -33,17 +33,15 @@ describe('environment', function () {
resolveAppiumHome.cache = new Map();
findAppiumDependencyPackage.cache = new Map();
readPackageInDir.cache = new Map();
oldEnvAppiumHome = process.env.APPIUM_HOME;
delete process.env.APPIUM_HOME;
});
after(async function () {
await fs.rimraf(cwd);
});
beforeEach(function () {
oldEnvAppiumHome = process.env.APPIUM_HOME;
delete process.env.APPIUM_HOME;
});
afterEach(function () {
process.env.APPIUM_HOME = oldEnvAppiumHome;
});

View File

@@ -100,7 +100,7 @@ describe('#zip', function () {
it('should stop iterating zipFile if onEntry callback returns false', async function () {
let i = 0;
// eslint-disable-next-line require-await
await zip.readEntries(zippedFilePath, async () => {
i++;
return false;
@@ -280,7 +280,7 @@ describe('#zip', function () {
const expectedPath = path.join(assetsPath, 'kanji-正世丕.app');
// we cannot use the `should` syntax because `fs.exists` resolves to a primitive (boolean)
if (!(await fs.exists(expectedPath))) {
throw new chai.AssertionError(`Expected ${expectedPath} to exist, but it does not`);
throw new Error(`Expected ${expectedPath} to exist, but it does not`);
}
});
});

View File

@@ -1,4 +1,4 @@
/* eslint-disable require-await */
// @ts-check
/**

View File

@@ -1,4 +1,4 @@
/* eslint-disable require-await */
// @ts-check
import path from 'path';

View File

@@ -75,8 +75,6 @@ describe('fs', function () {
(await fs.readFile(newPath, 'utf8')).should.contain('readFile');
});
it('should be able to copy a directory');
it('should throw an error if the source does not exist', async function () {
await fs.copyFile('/sdfsdfsdfsdf', '/tmp/bla').should.eventually.be.rejected;
});

View File

@@ -2,7 +2,7 @@ import {node} from '../../lib';
import path from 'path';
import _ from 'lodash';
describe('node utilities', async function () {
describe('node utilities', function () {
let should;
before(async function () {

View File

@@ -44,6 +44,4 @@ describe('npm', function () {
(null === npm.getLatestSafeUpgradeFromVersions('10', versions1)).should.be.true;
});
});
it('should have many more unit tests');
});

View File

@@ -80,7 +80,7 @@ describe('process', function () {
await process.killProcess('tail');
// it may take a moment to actually be registered as killed
// eslint-disable-next-line require-await
await retryInterval(10, 100, async () => {
proc.isRunning.should.be.false;
});
@@ -88,7 +88,7 @@ describe('process', function () {
it('should do nothing if the process does not exist', async function () {
proc.isRunning.should.be.true;
await process.killProcess('asdfasdfasdf');
// eslint-disable-next-line require-await
await retryInterval(10, 100, async () => {
proc.isRunning.should.be.false;
}).should.eventually.be.rejected;

View File

@@ -16,9 +16,11 @@ import sinon from 'sinon';
export function withMocks(mockDefs, fn) {
return () => {
const mocks = new MockStore();
// eslint-disable-next-line mocha/no-top-level-hooks
beforeEach(function withMocksBeforeEach() {
mocks.createMocks(mockDefs);
});
// eslint-disable-next-line mocha/no-top-level-hooks
afterEach(function withMocksAfterEach() {
mocks.reset();
});

View File

@@ -17,9 +17,11 @@ export function withSandbox(mockDefs, fn) {
return () => {
/** @type {SandboxStore} */
const sbx = new SandboxStore();
// eslint-disable-next-line mocha/no-top-level-hooks
beforeEach(function beforeEach() {
sbx.createSandbox(mockDefs);
});
// eslint-disable-next-line mocha/no-top-level-hooks
afterEach(function afterEach() {
sbx.reset();
});

View File

@@ -138,8 +138,14 @@ export type ExecuteMethodMap<T extends Plugin | Driver> = T extends Plugin
? Readonly<StringRecord<DriverExecuteMethodDef<T>>>
: never;
export interface BidiMethodParams {
required?: readonly string[];
optional?: readonly string[];
};
export interface BidiMethodDef extends BaseExecuteMethodDef {
command: string;
params?: BidiMethodParams;
}
export interface BidiMethodMap {
@@ -149,3 +155,25 @@ export interface BidiMethodMap {
export interface BidiModuleMap {
[k: string]: BidiMethodMap;
}
// https://w3c.github.io/webdriver-bidi/#protocol-definition
export interface GenericBiDiCommandResponse {
id: number;
[key: string]: any;
}
export interface BiDiResultData {
[key: string]: any;
}
export interface SuccessBiDiCommandResponse extends GenericBiDiCommandResponse {
type: 'success';
result: BiDiResultData;
}
export interface ErrorBiDiCommandResponse extends GenericBiDiCommandResponse {
type: 'error';
error: string;
message: string;
stacktrace?: string;
}

View File

@@ -2,7 +2,7 @@ import type {EventEmitter} from 'node:events';
import type {Merge} from 'type-fest';
import type {ActionSequence} from './action';
import type {Capabilities, DriverCaps, W3CCapabilities, W3CDriverCaps} from './capabilities';
import type {ExecuteMethodMap, MethodMap} from './command';
import type {BidiModuleMap, BiDiResultData, ExecuteMethodMap, MethodMap} from './command';
import type {ServerArgs} from './config';
import type {HTTPHeaders, HTTPMethod} from './http';
import type {AppiumLogger} from './logger';
@@ -350,6 +350,7 @@ export interface ILogCommands {
export interface IBidiCommands {
bidiSubscribe(events: string[], contexts: string[]): Promise<void>;
bidiUnsubscribe(events: string[], contexts: string[]): Promise<void>;
bidiStatus(): Promise<DriverStatus>;
}
/**
@@ -576,6 +577,12 @@ export interface EventHistoryCommand {
export type Protocol = 'MJSONWP' | 'W3C';
export interface DriverStatus {
ready: boolean,
message: string,
[key: string]: any;
}
/**
* Methods and properties which both `AppiumDriver` and `BaseDriver` inherit.
*
@@ -604,6 +611,7 @@ export interface Core<C extends Constraints, Settings extends StringRecord = Str
eventHistory: EventHistory;
bidiEventSubs: Record<string, string[]>;
doesSupportBidi: boolean;
updateBidiCommands(cmds: BidiModuleMap): void;
onUnexpectedShutdown(handler: () => any): void;
/**
* @summary Retrieve the server's current status.
@@ -704,7 +712,7 @@ export interface Driver<
* @param bidiCmd - the name of the command in the bidi spec
* @param args - arguments to pass to the command
*/
executeBidiCommand(bidiCmd: string, ...args: any[]): Promise<any>;
executeBidiCommand(bidiCmd: string, ...args: any[]): Promise<BiDiResultData>;
/**
* Signify to any owning processes that this driver encountered an error which should cause the
@@ -1991,6 +1999,23 @@ export interface DriverStatic<T extends Driver> {
baseVersion: string;
updateServer?: UpdateServerCallback;
newMethodMap?: MethodMap<T>;
/**
* Drivers can define new custom bidi commands and map them to driver methods. The format must
* be the same as that used by Appium's bidi-commands.js file, for example:
* @example
* {
* myNewBidiModule: {
* myNewBidiCommand: {
* command: 'driverMethodThatWillBeCalled',
* params: {
* required: ['requiredParam'],
* optional: ['optionalParam'],
* }
* }
* }
* }
*/
newBidiCommands?: BidiModuleMap;
executeMethodMap?: ExecuteMethodMap<T>;
}

View File

@@ -1,5 +1,5 @@
import {AsyncReturnType} from 'type-fest';
import {ExecuteMethodMap, MethodMap} from './command';
import {BidiModuleMap, ExecuteMethodMap, MethodMap} from './command';
import {DriverCommand, ExternalDriver} from './driver';
import {AppiumLogger} from './logger';
import {UpdateServerCallback} from './server';

View File

@@ -43,7 +43,7 @@
"@appium/tsconfig": "^0.3.3",
"@types/express": "5.0.0",
"@types/ws": "8.5.13",
"type-fest": "4.30.0"
"type-fest": "4.31.0"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",

View File

@@ -1,4 +1,4 @@
/* eslint-disable no-case-declarations */
import {BasePlugin} from 'appium/plugin';
import {errors} from 'appium/driver';

View File

@@ -38,7 +38,7 @@
"dependencies": {
"@types/xmldom": "0.1.34",
"@xmldom/xmldom": "0.9.6",
"fast-xml-parser": "4.5.0",
"fast-xml-parser": "4.5.1",
"lodash": "4.17.21",
"source-map-support": "0.5.21",
"xpath": "0.0.34"

View File

@@ -7,7 +7,7 @@ import {
} from '../fixtures';
import {transformAttrs, transformChildNodes, transformSourceXml} from '../../lib/source';
describe('source functions', async function () {
describe('source functions', function () {
before(async function () {
const chai = await import('chai');
chai.should();

View File

@@ -2,7 +2,7 @@ import {runQuery, transformQuery, getNodeAttrVal} from '../../lib/xpath';
import {transformSourceXml} from '../../lib/source';
import {XML_IOS} from '../fixtures';
describe('xpath functions', async function () {
describe('xpath functions', function () {
let should;
before(async function () {
const chai = await import('chai');