mirror of
https://github.com/appium/appium.git
synced 2026-01-06 02:09:59 -06:00
Merge branch 'master' into appium3
This commit is contained in:
@@ -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/**
|
||||
62
.eslintrc
62
.eslintrc
@@ -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`"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
5
.github/labeler.yml
vendored
@@ -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:
|
||||
|
||||
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -44,6 +44,9 @@ on:
|
||||
- '!**/sample-code/**'
|
||||
- '!packages/*/docs/**'
|
||||
|
||||
env:
|
||||
CI: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
||||
23
eslint.config.mjs
Normal file
23
eslint.config.mjs
Normal 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
7175
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 base–driver 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
|
||||
|
||||
@@ -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
436
packages/appium/lib/bidi.ts
Normal 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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"webdriverio": "8.40.6"
|
||||
"webdriverio": "9.4.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable require-await */
|
||||
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable require-await */
|
||||
|
||||
import _ from 'lodash';
|
||||
import B from 'bluebird';
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ const BIDI_COMMANDS = /** @type {const} */ ({
|
||||
command: 'bidiUnsubscribe',
|
||||
params: SUBSCRIPTION_REQUEST_PARAMS,
|
||||
},
|
||||
status: {
|
||||
command: 'bidiStatus',
|
||||
params: {},
|
||||
}
|
||||
},
|
||||
browsingContext: {
|
||||
navigate: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable require-await */
|
||||
|
||||
import {errors, BaseDriver, determineProtocol} from '../../../lib';
|
||||
import {PROTOCOLS} from '../../../lib/constants';
|
||||
import {util} from '@appium/support';
|
||||
|
||||
@@ -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'});
|
||||
};
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import BaseDriver from '../../../lib';
|
||||
import {BaseDriver} from '../../../lib';
|
||||
import {driverUnitTestSuite} from '@appium/driver-test-support';
|
||||
|
||||
driverUnitTestSuite(BaseDriver, {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-check
|
||||
|
||||
import BaseDriver from '../../../lib';
|
||||
import {BaseDriver} from '../../../lib';
|
||||
import {createSandbox} from 'sinon';
|
||||
|
||||
describe('timeout', function () {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// transpile:mocha
|
||||
|
||||
import {welcome} from '../../../lib/express/static';
|
||||
import {createSandbox} from 'sinon';
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
|
||||
|
||||
/**
|
||||
* Main CLI entry point for `@appium/docutils`
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
231
packages/eslint-config-appium-ts/index.mjs
Normal file
231
packages/eslint-config-appium-ts/index.mjs
Normal 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/**',
|
||||
'**/.*',
|
||||
],
|
||||
}
|
||||
|
||||
];
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
[](https://npmjs.org/package/@appium/eslint-config-appium)
|
||||
[](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
|
||||
@@ -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'}},
|
||||
],
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
const {doctor} = require('appium/support');
|
||||
|
||||
/** @satisfies {import('@appium/types').IDoctorCheck} */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
const {EnvVarAndPathCheck} = require('./common');
|
||||
|
||||
const fakeCheck1 = new EnvVarAndPathCheck('FAKE1');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
const {EnvVarAndPathCheck} = require('./common');
|
||||
|
||||
const fakeCheck2 = new EnvVarAndPathCheck('FAKE2');
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import FakePlugin from '../../lib/plugin';
|
||||
import {FakePlugin} from '../../lib/plugin';
|
||||
import B from 'bluebird';
|
||||
|
||||
class FakeExpress {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
|
||||
|
||||
import _ from 'lodash';
|
||||
import {errors} from 'appium/driver';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable require-await */
|
||||
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable require-await */
|
||||
|
||||
// @ts-check
|
||||
|
||||
import path from 'path';
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -44,6 +44,4 @@ describe('npm', function () {
|
||||
(null === npm.getLatestSafeUpgradeFromVersions('10', versions1)).should.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('should have many more unit tests');
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
|
||||
|
||||
import {BasePlugin} from 'appium/plugin';
|
||||
import {errors} from 'appium/driver';
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user