Compare commits

..
Author SHA1 Message Date
Jonathan Lipps 59dd30ba20 Publish
- appium@2.0.0-beta.35
 - @appium/doctor@1.16.19
 - @appium/eslint-config-appium@5.1.1
 - @appium/gulp-plugins@6.0.10
2022-05-03 13:42:56 -07:00
Christopher Hiller 6480679bfb chore(appium): lint again 2022-05-03 13:34:10 -07:00
Christopher Hiller c2753107c6 test(appium): e2e helpers now log executed child process commands 2022-05-03 13:12:31 -07:00
Christopher Hiller a794ade7b0 chore(appium): lint 2022-05-03 13:09:17 -07:00
Christopher Hiller 5e24fbac5c chore(ci): add Node v18 to matrix; remove Node v17 2022-05-03 12:50:20 -07:00
Renovate Bot 8caf18f542 chore(gulp-plugins): update dependency mocha to v10 2022-05-03 12:21:06 -07:00
Christopher Hiller 93e05a8269 fix(eslint-config-appium): revert prettier-related change
This reverts commits b34b75a80e,  88a6655253 and d89203f96c which, as a whole, was breaking.

This will fix the publish of v5.1.0, which should have been a major release.

After v5.1.1 has been released, this PR should be reverted in prep for v6.
2022-05-03 12:19:13 -07:00
Christopher Hiller 5cc22325f2 chore(appium): refactor some variable/parameter names for clarity
renamed parameters named `ext` to `installSpec`, since these correspond to the values of `installSpec` in the manifest
2022-05-03 12:13:44 -07:00
Christopher Hiller a2b07f6150 test(appium): replace all occurrences of 'driver'/'plugin' literals w/ constants 2022-05-03 12:04:15 -07:00
Christopher Hiller 823014321a chore: do not use @babel/register w/ wallaby 2022-05-03 12:04:02 -07:00
Christopher Hiller d89203f96c fix(eslint-config-appium): remove custom indenting rules
these rules also conflict with prettier. prettier does something very similar, but not exactly.  w/o this, running `prettier` then `eslint` will cause an eslint error.
2022-05-03 11:42:30 -07:00
Jonathan Lipps 773d9d6c0d docs(appium): add drivers intro doc (resolves #16795) 2022-05-03 10:27:40 -07:00
Renovate Bot a686053f03 chore(doctor): update dependency appium-adb to v9.2.2 2022-05-03 11:19:47 +00:00
Christopher Hiller 5a3a0f7c14 chore(appium): remove unused appium-config.schema.json from vcs 2022-05-02 15:28:33 -07:00
28 changed files with 675 additions and 611 deletions
-1
View File
@@ -1 +0,0 @@
* text=auto eol=lf
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x, 17.x]
node-version: [14.x, 16.x, 18.x]
steps:
- uses: actions/checkout@v3
+17 -24
View File
@@ -19,8 +19,6 @@ module.exports = (wallaby) => {
'!./packages/*/gulpfile.js',
'!./packages/*/scripts/**',
'./packages/*/test/**/fixtures/**/*',
'./packages/*/test/**/mocks.js',
'./packages/*/test/helpers.js',
'./babel.config.json',
// below this are fixtures
{
@@ -35,34 +33,29 @@ module.exports = (wallaby) => {
instrument: false,
pattern: './packages/base-driver/static/**/*',
},
{
instrument: false,
pattern: './packages/gulp-plugins/build/**/*',
},
// this setup file is special
{
instrument: false,
pattern: './test/setup.js'
},
'!**/local_appium_home/**'
'!**/local_appium_home/**',
],
testFramework: 'mocha',
tests: [
'./packages/*/test/**/*-specs.js',
'./packages/*/test/unit/**/*.spec.js',
'!./packages/*/test/**/*-e2e-specs.js',
'!./packages/*/test/e2e/**/*',
'!./packages/*/node_modules/**',
// this is more of an E2E test and it's tedious to run
'!./packages/gulp-plugins/test/transpile-specs.js',
'!**/local_appium_home/**'
],
tests: ['./packages/*/test/unit/**/*.spec.js'],
workers: {
restart: true,
},
setup() {
require('./test/setup');
// This copied out of `./test/setup.js`, which uses `@babel/register`.
// Wallaby doesn't need `@babel/register` (and it probably makes Wallaby slow),
// but we need the other stuff, so here it is.
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const sinonChai = require('sinon-chai');
// The `chai` global is set if a test needs something special.
// Most tests won't need this.
global.chai = chai.use(chaiAsPromised).use(sinonChai);
// `should()` is only necessary when working with some `null` or `undefined` values.
global.should = chai.should();
},
runMode: 'onsave'
runMode: 'onsave',
};
};
+5 -13
View File
@@ -29,11 +29,11 @@
"build:loose": "run-s -c build:distfiles build:types || true",
"build:types": "tsc -b",
"clean": "run-s clean:artifacts clean:packages clean:monorepo",
"clean:artifacts": "run-s clean:distfiles clean:types && npx rimraf \"packages/*/build\"",
"clean:distfiles": "npx rimraf \"packages/*/build/**/*.js\"",
"clean:monorepo": "npx rimraf node_modules package-lock.json",
"clean:packages": "lerna clean -y && npx rimraf \"packages/*/package-lock.json\"",
"clean:types": "tsc -b --clean",
"clean:artifacts": "run-s clean:distfiles clean:types && npx rimraf \"packages/*/build\"",
"dev": "run-p dev:*",
"dev:distfiles": "lerna run --parallel --prefix --concurrency=8 dev",
"dev:types": "tsc -b --watch",
@@ -70,14 +70,7 @@
"precommit-lint"
],
"lint-staged": {
"*.js": [
"eslint --fix",
"prettier --write"
]
},
"prettier": {
"bracketSpacing": false,
"singleQuote": true
"*.js": "eslint --fix"
},
"dependencies": {
"@appium/base-driver": "file:packages/base-driver",
@@ -85,7 +78,6 @@
"@appium/doctor": "file:packages/doctor",
"@appium/docutils": "file:packages/docutils",
"@appium/eslint-config-appium": "file:packages/eslint-config-appium",
"@appium/execute-driver-plugin": "file:packages/execute-driver-plugin",
"@appium/fake-driver": "file:packages/fake-driver",
"@appium/fake-plugin": "file:packages/fake-plugin",
"@appium/gulp-plugins": "file:packages/gulp-plugins",
@@ -97,6 +89,7 @@
"@appium/test-support": "file:packages/test-support",
"@appium/types": "file:packages/types",
"@appium/universal-xml-plugin": "file:packages/universal-xml-plugin",
"@appium/execute-driver-plugin": "file:packages/execute-driver-plugin",
"appium": "file:packages/appium"
},
"devDependencies": {
@@ -149,7 +142,6 @@
"chai-as-promised": "7.1.1",
"chai-webdriverio-async": "2.7.0",
"eslint": "7.32.0",
"eslint-config-prettier": "8.5.0",
"eslint-find-rules": "4.1.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-mocha": "9.0.0",
@@ -164,15 +156,15 @@
"lint-staged": "12.4.1",
"log-symbols": "4.1.0",
"mjpeg-server": "0.3.1",
"mocha": "9.2.2",
"mocha": "10.0.0",
"npm-run-all": "4.1.5",
"pre-commit": "1.2.2",
"prettier": "2.6.2",
"rewiremock": "3.14.3",
"rimraf": "3.0.2",
"serve-static": "1.15.0",
"sinon": "13.0.2",
"sinon-chai": "3.7.0",
"strip-ansi": "6.0.1",
"sync-monorepo-packages": "0.3.5",
"through2": "4.0.2",
"type-fest": "2.12.2",
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.0.0-beta.35](https://github.com/appium/appium/compare/appium@2.0.0-beta.34...appium@2.0.0-beta.35) (2022-05-03)
**Note:** Version bump only for package appium
# [2.0.0-beta.34](https://github.com/appium/appium/compare/appium@2.0.0-beta.33...appium@2.0.0-beta.34) (2022-05-02)
**Note:** Version bump only for package appium
+184 -1
View File
@@ -2,4 +2,187 @@
title: Intro to Appium Drivers
---
TODO
As the [main Overview](index.md) makes clear, "drivers" are basically Appium's answer to the
question, "how do we support automation of multiple, unrelated platforms?" In this doc we'll get
into a little more detail about how drivers work. The specific details of how drivers work probably
don't matter too much for you, unless you're planning on writing your own driver or contributing to
an existing driver (things we hope you do!).
The main benefit in understanding a bit more of how drivers work is that being aware of the typical
complexity or the typical driver architecture will inform your debugging process when you
inevitably run into an issue in one of your tests.
## Interface Implementations
At the most basic level, drivers are simply Node.js classes that extend a special class included in
Appium, called `BaseDriver`. You could have something very close to a "working" driver, with these
very simple lines of code:
```js
import BaseDriver from '@appium/base-driver'
class MyNewDriver extends BaseDriver {
}
```
This empty driver doesn't *do* anything, but you could wrap it up in a Node.js module, add a few
Appium-related fields to the module's manifest (`package.json`), and then install it using `appium
driver install`.
So, from a technical perspective, an Appium driver is just a bit of code that inherits from some
other Appium code. That's it! Now, inheriting from `BaseDriver` actually gives us a lot, because
`BaseDriver` is essentially an encapsulation of the entire WebDriver protocol. So all a driver
needs to do to do something useful is to *implement* Node.js methods with names corresponding to
their WebDriver protocol equivalents.
So let's say I wanted to do something with this empty driver; first I have to decide which
WebDriver command I want to implement. For our example, let's take the [Navigate
To](https://www.w3.org/TR/webdriver1/#navigate-to) WebDriver command. Leave aside for the moment
what I want to have the driver *do* when this command is executed. To tell Appium the driver can
handle the command, all we have to do is define a method like this in our driver class:[^1]
```js
async setUrl(url) {
// do whatever we want here
}
```
[^1]: You might notice that `setUrl` doesn't look anything like `Navigate To`, so how did we know
to use it rather than some other random string? Well, Appium's WebDriver-protocol-to-method-name
mapping is defined in a special file within the `@appium/base-driver` package called
[routes.js](https://github.com/appium/appium/blob/2.0/packages/base-driver/lib/protocol/routes.js).
So if you're writing a driver, this is where you would go to figure out what method names to use
and what parameters to expect. Or you could look at the source for any of the main Appium
drivers!
That's it! How we actually implement the command is totally up to us, and depends on the
platform(s) we want to support. Here are some different example implementations of this command for
different platforms:
- Browsers: execute some JavaScript to set `window.location.href`
- iOS apps: launch an app using a deep link
- Android apps: launch an app using a deep link
- React apps: load a specific route
- Unity: go to a named scene
So you can see there can be a lot of differences between how drivers implement the same WebDriver
command across platforms.[^2] What is the *same*, though, is how they express that they can handle
a protocol command.
[^2]: Of course, we want to keep the semantics as similar as possible, but in the world of iOS, for
example, launching an app via a deep link (a URL with a special app-specific scheme) is about as
close as we are going to get to navigating to a web URL.
We're going into this great amount of detail (which you don't need to remember, by the way),
because it's important to stress the point that an Appium driver is not inherently anything in
particular, other than a bit of JS code that can handle WebDriver protocol commands. Where you go
from there is up to you, the driver author!
## Automation mapping
But *typically* what driver authors want to do is to provide automation behaviours for a given
platform(s) that are semantically very similar to the the WebDriver spec implementations for
browsers. When you want to find an element, you should get a reference to a UI element. When you
want to click or tap that element, the resulting behaviour should be the same as if a person were
to click or tap on the element. And so on.
So the real challenge for driver authors is not how to work with the WebDriver protocol (because
`BaseDriver` encapsulates all that for you), but how to make the actual automation happen on the
target platform. Every driver relies on its own set of underlying technologies here. As mentioned
in the [Overview](index.md), the iOS driver uses an Apple technology called
[XCUITest](https://developer.apple.com/documentation/xctest/xcuielement). These underlying
automation technologies usually have proprietary or idiosyncratic APIs of their own. Writing
a driver becomes the task of mapping the WebDriver protocol to this underlying API (or sometimes
a set of different underlying APIs--for example, the UiAutomator2 driver relies not only on the
[UiAutomator2](https://developer.android.com/training/testing/other-components/ui-automator)
technology from Google, but also functions only available through
[ADB](https://developer.android.com/studio/command-line/adb), as well as functions only available
via the Android SDK inside a helper app). Tying it all together into a single, usable, WebDriver
interface is the incredibly useful (but incredibly challenging) art of driver development!
## Multi-level architecture
In practice, this often results in a pretty complex architecture. Let's take iOS for example again.
The XCUITest framework (the one used by the Appium driver) expects code that calls it to be written
in Objective-C or Swift. Furthermore, XCUITest code can only be run in a special mode triggered by
Xcode (and directly or indirectly, the Xcode command line tools). In other words, there's no
straightforward way to go from a Node.js function implementation (like `setUrl()` above) to
XCUITest API calls.
What the XCUITest driver authors have done is instead to split the driver into two parts: one part
written in Node.js (the part which is incorporated into Appium and which initially handles the
WebDriver commands), and the other part written in Objective-C (the part which actually gets run on
an iOS device and makes XCUITest API calls). This makes interfacing with XCUITest possible, but
introduces the new problem of coordination between the two parts.
The driver authors could have chosen any of a number of very different strategies to model the
communication between the Node.js side and the Objective-C side, but at the end of the day decided
to use ... the WebDriver protocol! That's right, the Objective-C side of the XCUITest driver is
itself a WebDriver implementation, called
[WebDriverAgent](https://github.com/appium/webdriveragent).[^3]
[^3]: You could in theory, therefore, point your WebDriver client straight to WebDriverAgent and
bypass Appium entirely. This is usually not convenient, however, for a few reasons:
- The Appium XCUITest driver builds and manages WebDriverAgent for you, which can be a pain and
involves the use of Xcode.
- The XCUITest driver does lots more than what can be done by WebDriverAgent, for example working
with simulators or devices, installing apps, and the like.
The moral of the story is that driver architectures can become quite complicated and multilayered,
due to the nature of the problem we're trying to solve. It also means it can be difficult sometimes
to tell where in this chain of technologies something has gone wrong, if you run into a problem
with a particular test. With the XCUITest world again, we have something like the following set of
technologies all in play at the same time:
- Your test code (in its programming language) - owned by you
- The Appium client library - owned by Appium
- The Selenium client library - owned by Selenium
- The network (local or Internet)
- The Appium server - owned by Appium
- The Appium XCUITest driver - owned by Appium
- WebDriverAgent - owned by Appium
- Xcode - owned by Apple
- XCUITest - owned by Apple
- iOS itself - owned by Apple
- macOS (where Xcode and iOS simulators run) - owned by Apple
It's a pretty deep stack!
## Proxy mode
There's one other important architectural aspect of drivers to understand. It can be exemplified
again by the XCUITest driver. Recall that we just discussed how the two "halves" of the XCUITest
driver both speak the WebDriver protocol---the Node.js half clicks right into Appium's WebDriver
server, and the Objective-c half (WebDriverAgent) is its own WebDriver implementation.
This opens up the possibility of Appium taking a shortcut in certain cases. Let's imagine that the
XCUITest driver needs to implement the `Click Element` command. The internal code of this
implementation would look something like taking the appropriate parameters and constructing an HTTP
request to the WebDriverAgent server. In this case, we're basically just reconstructing the
client's original call to the Appium server![^4] So there's really no need to even write a function
implementing the `Click Element` command. Instead, the XCUITest driver can just let Appium know
that this command should be proxied directly to some other WebDriver server.
[^4]: It's not *exactly* the same call, because the Appium server and the WebDriverAgent server
will generate different session IDs, but these differences will be handled transparently.
If you're not familiar with the concept of "proxying," in this case it just means that the XCUITest
driver will not be involved at all in handling the command. Instead it will merely be repackaged
and forwarded to WebDriverAgent at the protocol level, and WebDriverAgent's response will likewise
be passed back directly to the client, without any XCUITest driver code seeing it or modifying it.
This architectural pattern provides a nice bonus for driver authors who choose to deal with the
WebDriver protocol everywhere, rather than constructing bespoke protocols. It also means that
Appium can create wrapper drivers for any other existing WebDriver implementation very easily. If
you look at the [Appium Safari driver](https://github.com/appium/appium-safari-driver) code, for
example, you'll see that it implements basically no standard commands, because all of these are
proxied directly to an underlying SafariDriver process.
It's important to understand that this proxying business is sometimes happening under the hood,
because if you're ever diving into some open source driver code trying to figure out where
a command is implemented, you might be surprised to find no implementation at all in the Node.js
driver code itself! In that case, you'll need to figure out where commands are being proxied to so
you can look there for the appropriate implementation.
OK, that's enough for this very detailed introduction to drivers!
@@ -1,278 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"additionalProperties": false,
"description": "A schema for Appium configuration files",
"properties": {
"server": {
"additionalProperties": false,
"description": "Configuration when running Appium as a server",
"properties": {
"address": {
"$comment": "I think hostname covers both DNS and IPv4...could be wrong",
"appiumCliAliases": [
"a"
],
"default": "0.0.0.0",
"description": "IP address to listen on",
"format": "hostname",
"title": "address config",
"type": "string"
},
"allow-cors": {
"description": "Whether the Appium server should allow web browser connections from any host",
"title": "allow-cors config",
"type": "boolean",
"default": false
},
"allow-insecure": {
"appiumCliTransformer": "csv",
"default": [],
"description": "Set which insecure features are allowed to run in this server's sessions. Features are defined on a driver level; see documentation for more details. Note that features defined via \"deny-insecure\" will be disabled, even if also listed here. If string, a path to a text file containing policy or a comma-delimited list.",
"items": {
"type": "string"
},
"title": "allow-insecure config",
"type": "array",
"uniqueItems": true
},
"base-path": {
"appiumCliAliases": [
"pa"
],
"default": "",
"description": "Base path to use as the prefix for all webdriver routes running on the server",
"title": "base-path config",
"type": "string"
},
"callback-address": {
"appiumCliAliases": [
"ca"
],
"description": "Callback IP address (default: same as \"address\")",
"title": "callback-address config",
"type": "string"
},
"callback-port": {
"appiumCliAliases": [
"cp"
],
"default": 4723,
"description": "Callback port (default: same as \"port\")",
"maximum": 65535,
"minimum": 1,
"title": "callback-port config",
"type": "integer"
},
"debug-log-spacing": {
"default": false,
"description": "Add exaggerated spacing in logs to help with visual inspection",
"title": "debug-log-spacing config",
"type": "boolean"
},
"default-capabilities": {
"$comment": "TODO",
"appiumCliAliases": [
"dc"
],
"description": "Set the default desired capabilities, which will be set on each session unless overridden by received capabilities. If a string, a path to a JSON file containing the capabilities, or raw JSON.",
"title": "default-capabilities config",
"type": "object"
},
"deny-insecure": {
"$comment": "Allowed values are defined by drivers",
"appiumCliTransformer": "csv",
"default": [],
"description": "Set which insecure features are not allowed to run in this server's sessions. Features are defined on a driver level; see documentation for more details. Features listed here will not be enabled even if also listed in \"allow-insecure\", and even if \"relaxed-security\" is enabled. If string, a path to a text file containing policy or a comma-delimited list.",
"items": {
"type": "string"
},
"title": "deny-insecure config",
"type": "array",
"uniqueItems": true
},
"driver": {
"description": "Driver-specific configuration. Keys should correspond to driver package names",
"properties": {},
"title": "driver config",
"type": "object"
},
"keep-alive-timeout": {
"appiumCliAliases": [
"ka"
],
"default": 600,
"description": "Number of seconds the Appium server should apply as both the keep-alive timeout and the connection timeout for all requests. A value of 0 disables the timeout.",
"minimum": 0,
"title": "keep-alive-timeout config",
"type": "integer"
},
"local-timezone": {
"default": false,
"description": "Use local timezone for timestamps",
"title": "local-timezone config",
"type": "boolean"
},
"log": {
"appiumCliAliases": [
"g"
],
"appiumCliDest": "logFile",
"description": "Also send log output to this file",
"title": "log config",
"type": "string"
},
"log-filters": {
"$comment": "TODO",
"description": "One or more log filtering rules",
"items": {
"type": "string"
},
"title": "log-filters config",
"type": "array"
},
"log-level": {
"appiumCliDest": "loglevel",
"default": "debug",
"description": "Log level (console[:file])",
"enum": [
"info",
"info:debug",
"info:info",
"info:warn",
"info:error",
"warn",
"warn:debug",
"warn:info",
"warn:warn",
"warn:error",
"error",
"error:debug",
"error:info",
"error:warn",
"error:error",
"debug",
"debug:debug",
"debug:info",
"debug:warn",
"debug:error"
],
"title": "log-level config",
"type": "string"
},
"log-no-colors": {
"default": false,
"description": "Do not use color in console output",
"title": "log-no-colors config",
"type": "boolean"
},
"log-timestamp": {
"default": false,
"description": "Show timestamps in console output",
"title": "log-timestamp config",
"type": "boolean"
},
"long-stacktrace": {
"default": false,
"description": "Add long stack traces to log entries. Recommended for debugging only.",
"title": "long-stacktrace config",
"type": "boolean"
},
"no-perms-check": {
"default": false,
"description": "Do not check that needed files are readable and/or writable",
"title": "no-perms-check config",
"type": "boolean"
},
"nodeconfig": {
"$comment": "Selenium Grid 3 is unmaintained and Selenium Grid 4 no longer supports this file.",
"description": "Path to configuration JSON file to register Appium as a node with Selenium Grid 3; otherwise the configuration itself",
"title": "nodeconfig config",
"type": "object"
},
"plugin": {
"description": "Plugin-specific configuration. Keys should correspond to plugin package names",
"properties": {},
"title": "plugin config",
"type": "object"
},
"port": {
"appiumCliAliases": [
"p"
],
"default": 4723,
"description": "Port to listen on",
"maximum": 65535,
"minimum": 1,
"title": "port config",
"type": "integer"
},
"relaxed-security": {
"default": false,
"description": "Disable additional security checks, so it is possible to use some advanced features, provided by drivers supporting this option. Only enable it if all the clients are in the trusted network and it's not the case if a client could potentially break out of the session sandbox. Specific features can be overridden by using \"deny-insecure\"",
"title": "relaxed-security config",
"type": "boolean",
"appiumCliDest": "relaxedSecurityEnabled"
},
"session-override": {
"default": false,
"description": "Enables session override (clobbering)",
"title": "session-override config",
"type": "boolean"
},
"strict-caps": {
"default": false,
"description": "Cause sessions to fail if desired caps are sent in that Appium does not recognize as valid for the selected device",
"title": "strict-caps config",
"type": "boolean"
},
"tmp": {
"appiumCliDest": "tmpDir",
"description": "Absolute path to directory Appium can use to manage temp files. Defaults to C:\\Windows\\Temp on Windows and /tmp otherwise.",
"title": "tmp config",
"type": "string"
},
"trace-dir": {
"description": "Absolute path to directory Appium can use to save iOS instrument traces; defaults to <tmp>/appium-instruments",
"title": "trace-dir config",
"type": "string"
},
"use-drivers": {
"appiumCliDescription": "A list of drivers to activate. Can be a comma-delimited string or path to CSV file. By default, all installed drivers will be activated.",
"default": [],
"description": "A list of drivers to activate. By default, all installed drivers will be activated.",
"items": {
"type": "string"
},
"title": "use-drivers config",
"type": "array",
"uniqueItems": true
},
"use-plugins": {
"appiumCliDescription": "A list of plugins to activate. Can be a comma-delimited string, path to CSV file, or the string \"all\" to use all installed plugins.",
"default": [],
"description": "A list of plugins to activate. To activate all plugins, the value should be an array with a single item \"all\".",
"items": {
"type": "string"
},
"title": "use-plugins config",
"type": "array",
"uniqueItems": true
},
"webhook": {
"$comment": "This should probably use a uri-template format to restrict the protocol to http/https",
"appiumCliAliases": [
"G"
],
"description": "Also send log output to this http listener",
"format": "uri",
"title": "webhook config",
"type": "string"
}
},
"title": "server config",
"type": "object"
}
},
"title": "Appium Configuration",
"type": "object"
}
+45 -20
View File
@@ -1,14 +1,19 @@
import _ from 'lodash';
import ExtensionCommand from './extension-command';
import { KNOWN_DRIVERS } from '../constants';
import {KNOWN_DRIVERS} from '../constants';
import '@colors/colors';
const REQ_DRIVER_FIELDS = ['driverName', 'automationName', 'platformNames', 'mainClass'];
const REQ_DRIVER_FIELDS = [
'driverName',
'automationName',
'platformNames',
'mainClass',
];
/**
* @extends {ExtensionCommand<import('../extension/manifest').DriverType>}
* @extends {ExtensionCommand<DriverType>}
*/
export default class DriverCommand extends ExtensionCommand {
/**
* @param {DriverCommandOptions} opts
*/
@@ -18,43 +23,63 @@ export default class DriverCommand extends ExtensionCommand {
}
async install ({driver, installType, packageName}) {
return await super._install({ext: driver, installType, packageName});
return await super._install({
installSpec: driver,
installType,
packageName,
});
}
async uninstall ({driver}) {
return await super._uninstall({ext: driver});
return await super._uninstall({installSpec: driver});
}
async update ({driver, unsafe}) {
return await super._update({ext: driver, unsafe});
return await super._update({installSpec: driver, unsafe});
}
async run ({driver, scriptName}) {
return await super._run({ext: driver, scriptName});
return await super._run({installSpec: driver, scriptName});
}
getPostInstallText ({extName, extData}) {
return `Driver ${extName}@${extData.version} successfully installed\n`.green +
`- automationName: ${extData.automationName.green}\n` +
`- platformNames: ${JSON.stringify(extData.platformNames).green}`;
return (
`Driver ${extName}@${extData.version} successfully installed\n`.green +
`- automationName: ${extData.automationName.green}\n` +
`- platformNames: ${JSON.stringify(extData.platformNames).green}`
);
}
validateExtensionFields (appiumPkgData) {
const missingFields = REQ_DRIVER_FIELDS.reduce((acc, field) => (
appiumPkgData[field] ? acc : [...acc, field]
), []);
/**
* Validates fields in `appium` field of `driverMetadata`
*
* For any `package.json` fields which a driver requires, validate the type of
* those fields on the `package.json` data, throwing an error if anything is
* amiss.
* @param {import('appium/types').ExtMetadata<DriverType>} driverMetadata
* @param {string} installSpec
*/
validateExtensionFields (driverMetadata, installSpec) {
const missingFields = REQ_DRIVER_FIELDS.reduce(
(acc, field) => (driverMetadata[field] ? acc : [...acc, field]),
[]
);
if (!_.isEmpty(missingFields)) {
throw new Error(`Installed driver did not expose correct fields for compability ` +
`with Appium. Missing fields: ${JSON.stringify(missingFields)}`);
throw new Error(
`Driver "${installSpec}" did not expose correct fields for compability ` +
`with Appium. Missing fields: ${JSON.stringify(missingFields)}`
);
}
}
}
/**
* @typedef DriverCommandOptions
* @property {import('../extension/extension-config').ExtensionConfig<import('../extension/manifest').DriverType>} config
* @property {import('../extension/extension-config').ExtensionConfig<DriverType>} config
* @property {boolean} json
*/
/**
* @typedef {import('appium/types').DriverType} DriverType
*/
+229 -123
View File
@@ -2,12 +2,16 @@
import _ from 'lodash';
import path from 'path';
import { npm, fs, util, env } from '@appium/support';
import { log, spinWith, RingBuffer } from './utils';
import { SubProcess } from 'teen_process';
import { INSTALL_TYPE_NPM, INSTALL_TYPE_GIT, INSTALL_TYPE_GITHUB,
INSTALL_TYPE_LOCAL } from '../extension/extension-config';
import { packageDidChange } from '../extension/package-changed';
import {npm, fs, util, env} from '@appium/support';
import {log, spinWith, RingBuffer} from './utils';
import {SubProcess} from 'teen_process';
import {
INSTALL_TYPE_NPM,
INSTALL_TYPE_GIT,
INSTALL_TYPE_GITHUB,
INSTALL_TYPE_LOCAL,
} from '../extension/extension-config';
import {packageDidChange} from '../extension/package-changed';
const UPDATE_ALL = 'installed';
@@ -80,24 +84,31 @@ class ExtensionCommand {
* @return {Promise<ExtensionListData>} map of extension names to extension data
*/
async list ({showInstalled, showUpdates}) {
const lsMsg = `Listing ${showInstalled ? 'installed' : 'available'} ${this.type}s`;
const lsMsg = `Listing ${showInstalled ? 'installed' : 'available'} ${
this.type
}s`;
const installedNames = Object.keys(this.config.installedExtensions);
const knownNames = Object.keys(this.knownExtensions);
const exts = [...installedNames, ...knownNames].reduce((acc, name) => {
if (!acc[name]) {
if (installedNames.includes(name)) {
acc[name] = {...this.config.installedExtensions[name], installed: true};
} else if (!showInstalled) {
acc[name] = {pkgName: this.knownExtensions[name], installed: false};
const exts = [...installedNames, ...knownNames].reduce(
(acc, name) => {
if (!acc[name]) {
if (installedNames.includes(name)) {
acc[name] = {
...this.config.installedExtensions[name],
installed: true,
};
} else if (!showInstalled) {
acc[name] = {pkgName: this.knownExtensions[name], installed: false};
}
}
}
return acc;
},
/**
* This accumulator contains either {@linkcode UninstalledExtensionLIstData} _or_
* {@linkcode InstalledExtensionListData} without upgrade information (which is added by the below code block)
* @type {Record<string,Partial<InstalledExtensionListData>|UninstalledExtensionListData>}
*/({}));
return acc;
},
/**
* This accumulator contains either {@linkcode UninstalledExtensionLIstData} _or_
* {@linkcode InstalledExtensionListData} without upgrade information (which is added by the below code block)
* @type {Record<string,Partial<InstalledExtensionListData>|UninstalledExtensionListData>}
*/ ({})
);
// if we want to show whether updates are available, put that behind a spinner
await spinWith(this.isJsonOutput, lsMsg, async () => {
@@ -113,11 +124,12 @@ class ExtensionCommand {
const updates = await this.checkForExtensionUpdate(ext);
data.updateVersion = updates.safeUpdate;
data.unsafeUpdateVersion = updates.unsafeUpdate;
data.upToDate = updates.safeUpdate === null && updates.unsafeUpdate === null;
data.upToDate =
updates.safeUpdate === null && updates.unsafeUpdate === null;
}
});
const listData = /** @type {ExtensionListData} */(exts);
const listData = /** @type {ExtensionListData} */ (exts);
// if we're just getting the data, short circuit return here since we don't need to do any
// formatting logic
@@ -125,16 +137,20 @@ class ExtensionCommand {
return listData;
}
for (const [
name,
data
] of _.toPairs(listData)) {
for (const [name, data] of _.toPairs(listData)) {
let installTxt = ' [not installed]'.grey;
let updateTxt = '';
let upToDateTxt = '';
let unsafeUpdateTxt = '';
if (data.installed) {
const {installType, installSpec, updateVersion, unsafeUpdateVersion, version, upToDate} = data;
const {
installType,
installSpec,
updateVersion,
unsafeUpdateVersion,
version,
upToDate,
} = data;
let typeTxt;
switch (installType) {
case INSTALL_TYPE_GIT:
@@ -147,7 +163,9 @@ class ExtensionCommand {
default:
typeTxt = '(NPM)';
}
installTxt = `@${version.yellow} ${('[installed ' + typeTxt + ']').green}`;
installTxt = `@${version.yellow} ${
('[installed ' + typeTxt + ']').green
}`;
if (showUpdates) {
if (updateVersion) {
@@ -157,12 +175,15 @@ class ExtensionCommand {
upToDateTxt = ` [Up to date]`.green;
}
if (unsafeUpdateVersion) {
unsafeUpdateTxt = ` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
unsafeUpdateTxt =
` [${unsafeUpdateVersion} available (potentially unsafe)]`.cyan;
}
}
}
console.log(`- ${name.yellow}${installTxt}${updateTxt}${upToDateTxt}${unsafeUpdateTxt}`);
console.log(
`- ${name.yellow}${installTxt}${updateTxt}${upToDateTxt}${unsafeUpdateTxt}`
);
}
return listData;
@@ -174,33 +195,53 @@ class ExtensionCommand {
* @param {InstallArgs} args
* @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data
*/
async _install ({ext, installType, packageName}) {
async _install ({installSpec, installType, packageName}) {
/** @type {ExtensionFields<typeof this.type>} */
let extData;
let installSpec = ext;
if (packageName && [INSTALL_TYPE_LOCAL, INSTALL_TYPE_NPM].includes(installType)) {
throw new Error(`When using --source=${installType}, cannot also use --package`);
if (
packageName &&
[INSTALL_TYPE_LOCAL, INSTALL_TYPE_NPM].includes(installType)
) {
throw new Error(
`When using --source=${installType}, cannot also use --package`
);
}
if (!packageName && [INSTALL_TYPE_GIT, INSTALL_TYPE_GITHUB].includes(installType)) {
throw new Error(`When using --source=${installType}, must also use --package`);
if (
!packageName &&
[INSTALL_TYPE_GIT, INSTALL_TYPE_GITHUB].includes(installType)
) {
throw new Error(
`When using --source=${installType}, must also use --package`
);
}
if (installType === INSTALL_TYPE_GITHUB) {
if (installSpec.split('/').length !== 2) {
throw new Error(`Github ${this.type} spec ${installSpec} appeared to be invalid; ` +
'it should be of the form <org>/<repo>');
throw new Error(
`Github ${this.type} spec ${installSpec} appeared to be invalid; ` +
'it should be of the form <org>/<repo>'
);
}
extData = await this.installViaNpm({ext: installSpec, pkgName: /** @type {string} */(packageName)});
extData = await this.installViaNpm({
installSpec,
pkgName: /** @type {string} */ (packageName),
});
} else if (installType === INSTALL_TYPE_GIT) {
// git urls can have '.git' at the end, but this is not necessary and would complicate the
// way we download and name directories, so we can just remove it
installSpec = installSpec.replace(/\.git$/, '');
extData = await this.installViaNpm({ext: installSpec, pkgName: /** @type {string} */(packageName)});
extData = await this.installViaNpm({
installSpec,
pkgName: /** @type {string} */ (packageName),
});
} else {
let pkgName, pkgVer;
if (installType === INSTALL_TYPE_LOCAL) {
pkgName = path.isAbsolute(installSpec) ? installSpec : path.resolve(installSpec);
pkgName = path.isAbsolute(installSpec)
? installSpec
: path.resolve(installSpec);
} else {
// at this point we have either an npm package or an appium verified extension
// name or a local path. both of which will be installed via npm.
@@ -227,8 +268,9 @@ class ExtensionCommand {
// check it exists and get the correct package
const knownNames = Object.keys(this.knownExtensions);
if (!_.includes(knownNames, name)) {
const msg = `Could not resolve ${this.type}; are you sure it's in the list ` +
`of supported ${this.type}s? ${JSON.stringify(knownNames)}`;
const msg =
`Could not resolve ${this.type}; are you sure it's in the list ` +
`of supported ${this.type}s? ${JSON.stringify(knownNames)}`;
throw new Error(msg);
}
pkgName = this.knownExtensions[name];
@@ -238,16 +280,18 @@ class ExtensionCommand {
}
}
extData = await this.installViaNpm({ext, pkgName, pkgVer});
extData = await this.installViaNpm({installSpec, pkgName, pkgVer});
}
const extName = extData[/** @type {string} */(`${this.type}Name`)];
delete extData[/** @type {string} */(`${this.type}Name`)];
const extName = extData[/** @type {string} */ (`${this.type}Name`)];
delete extData[/** @type {string} */ (`${this.type}Name`)];
if (this.config.isInstalled(extName)) {
throw new Error(`A ${this.type} named '${extName}' is already installed. ` +
`Did you mean to update? 'appium ${this.type} update'. See ` +
`installed ${this.type}s with 'appium ${this.type} list --installed'.`);
throw new Error(
`A ${this.type} named '${extName}' is already installed. ` +
`Did you mean to update? 'appium ${this.type} update'. See ` +
`installed ${this.type}s with 'appium ${this.type} list --installed'.`
);
}
const extManifest = {...extData, installType, installSpec};
@@ -269,19 +313,25 @@ class ExtensionCommand {
*
* @param {InstallViaNpmArgs} args
*/
async installViaNpm ({ext, pkgName, pkgVer}) {
async installViaNpm ({installSpec, pkgName, pkgVer}) {
const npmSpec = `${pkgName}${pkgVer ? '@' + pkgVer : ''}`;
const specMsg = npmSpec === ext ? '' : ` using NPM install spec '${npmSpec}'`;
const msg = `Installing '${ext}'${specMsg}`;
const specMsg =
npmSpec === installSpec ? '' : ` using NPM install spec '${npmSpec}'`;
const msg = `Installing '${installSpec}'${specMsg}`;
try {
const pkgJsonData = await spinWith(this.isJsonOutput, msg, async () => (
await npm.installPackage(this.config.appiumHome, pkgName, {
pkgVer
})
));
return this.getExtensionFields(pkgJsonData);
const pkgJsonData = await spinWith(
this.isJsonOutput,
msg,
async () =>
await npm.installPackage(this.config.appiumHome, pkgName, {
pkgVer,
})
);
return this.getExtensionFields(pkgJsonData, installSpec);
} catch (err) {
throw new Error(`Encountered an error when installing package: ${err.message}`);
throw new Error(
`Encountered an error when installing package: ${err.message}`
);
}
}
@@ -303,31 +353,38 @@ class ExtensionCommand {
* load as the main driver class, or to be able to detect incompatibilities between driver and
* appium versions.
*
* @param {ExtPackageJson<ExtType>} pkgJsonData - the package.json data for a driver module, as if it had been
* straightforwardly 'require'd
* @param {ExtPackageJson<ExtType>} pkgJsonData - the package.json data for a driver module, as if it had been straightforwardly 'require'd
* @param {string} installSpec
* @returns {ExtensionFields<ExtType>}
*/
getExtensionFields (pkgJsonData) {
getExtensionFields (pkgJsonData, installSpec) {
if (!pkgJsonData.appium) {
throw new Error(`Installed driver did not have an 'appium' section in its ` +
`package.json file as expected`);
throw new Error(
`Installed driver did not have an 'appium' section in its ` +
`package.json file as expected`
);
}
const {appium, name, version} = pkgJsonData;
this.validateExtensionFields(appium);
this.validateExtensionFields(appium, installSpec);
/** @type {unknown} */
const result = {...appium, pkgName: name, version};
return /** @type {ExtensionFields<ExtType>} */(result);
return /** @type {ExtensionFields<ExtType>} */ (result);
}
/**
* For any package.json fields which a particular type of extension requires, validate the
* presence and form of those fields on the package.json data, throwing an error if anything is
/**
* For any `package.json` fields which a particular type of extension requires, validate the
* presence and form of those fields on the `package.json` data, throwing an error if anything is
* amiss.
*
* @param {ExtMetadata<ExtType>} appiumPkgData - the data in the "appium" field of package.json for an extension
* @param {ExtMetadata<ExtType>} extMetadata - the data in the "appium" field of `package.json` for an extension
* @param {string} installSpec - Extension name/spec
*/
// eslint-disable-next-line no-unused-vars
validateExtensionFields (appiumPkgData) {
validateExtensionFields (extMetadata, installSpec) {
throw new Error('Must be implemented in final class');
}
@@ -337,17 +394,22 @@ class ExtensionCommand {
* @param {UninstallOpts} opts
* @return {Promise<ExtRecord<ExtType>>} map of all installed extension names to extension data
*/
async _uninstall ({ext}) {
if (!this.config.isInstalled(ext)) {
throw new Error(`Can't uninstall ${this.type} '${ext}'; it is not installed`);
async _uninstall ({installSpec}) {
if (!this.config.isInstalled(installSpec)) {
throw new Error(
`Can't uninstall ${this.type} '${installSpec}'; it is not installed`
);
}
const installPath = this.config.getInstallPath(ext);
const installPath = this.config.getInstallPath(installSpec);
try {
await fs.rimraf(installPath);
} finally {
await this.config.removeExtension(ext);
await this.config.removeExtension(installSpec);
}
log(this.isJsonOutput, `Successfully uninstalled ${this.type} '${ext}'`.green);
log(
this.isJsonOutput,
`Successfully uninstalled ${this.type} '${installSpec}'`.green
);
return this.config.installedExtensions;
}
@@ -357,13 +419,17 @@ class ExtensionCommand {
* @param {ExtensionUpdateOpts} updateSpec
* @return {Promise<ExtensionUpdateResult>}
*/
async _update ({ext, unsafe}) {
const shouldUpdateAll = ext === UPDATE_ALL;
async _update ({installSpec, unsafe}) {
const shouldUpdateAll = installSpec === UPDATE_ALL;
// if we're specifically requesting an update for an extension, make sure it's installed
if (!shouldUpdateAll && !this.config.isInstalled(ext)) {
throw new Error(`The ${this.type} '${ext}' was not installed, so can't be updated`);
if (!shouldUpdateAll && !this.config.isInstalled(installSpec)) {
throw new Error(
`The ${this.type} '${installSpec}' was not installed, so can't be updated`
);
}
const extsToUpdate = shouldUpdateAll ? Object.keys(this.config.installedExtensions) : [ext];
const extsToUpdate = shouldUpdateAll
? Object.keys(this.config.installedExtensions)
: [installSpec];
// 'errors' will have ext names as keys and error objects as values
/** @type {Record<string,Error>} */
@@ -376,24 +442,40 @@ class ExtensionCommand {
for (const e of extsToUpdate) {
try {
await spinWith(this.isJsonOutput, `Checking if ${this.type} '${e}' is updatable`, () => {
if (this.config.installedExtensions[e].installType !== INSTALL_TYPE_NPM) {
throw new NotUpdatableError();
await spinWith(
this.isJsonOutput,
`Checking if ${this.type} '${e}' is updatable`,
() => {
if (
this.config.installedExtensions[e].installType !==
INSTALL_TYPE_NPM
) {
throw new NotUpdatableError();
}
}
});
const update = await spinWith(this.isJsonOutput, `Checking if ${this.type} '${e}' needs an update`, async () => {
const update = await this.checkForExtensionUpdate(e);
if (!(update.safeUpdate || update.unsafeUpdate)) {
throw new NoUpdatesAvailableError();
);
const update = await spinWith(
this.isJsonOutput,
`Checking if ${this.type} '${e}' needs an update`,
async () => {
const update = await this.checkForExtensionUpdate(e);
if (!(update.safeUpdate || update.unsafeUpdate)) {
throw new NoUpdatesAvailableError();
}
return update;
}
return update;
});
);
if (!unsafe && !update.safeUpdate) {
throw new Error(`The ${this.type} '${e}' has a major revision update ` +
`(${update.current} => ${update.unsafeUpdate}), which could include ` +
`breaking changes. If you want to apply this update, re-run with --unsafe`);
throw new Error(
`The ${this.type} '${e}' has a major revision update ` +
`(${update.current} => ${update.unsafeUpdate}), which could include ` +
`breaking changes. If you want to apply this update, re-run with --unsafe`
);
}
const updateVer = unsafe && update.unsafeUpdate ? update.unsafeUpdate : update.safeUpdate;
const updateVer =
unsafe && update.unsafeUpdate
? update.unsafeUpdate
: update.safeUpdate;
await spinWith(
this.isJsonOutput,
`Updating driver '${e}' from ${update.current} to ${updateVer}`,
@@ -407,12 +489,18 @@ class ExtensionCommand {
log(this.isJsonOutput, 'Update report:');
for (const [e, update] of _.toPairs(updates)) {
log(this.isJsonOutput, `- ${this.type} ${e} updated: ${update.from} => ${update.to}`.green);
log(
this.isJsonOutput,
`- ${this.type} ${e} updated: ${update.from} => ${update.to}`.green
);
}
for (const [e, err] of _.toPairs(errors)) {
if (err instanceof NotUpdatableError) {
log(this.isJsonOutput, `- '${e}' was not installed via npm, so we could not check ` +
`for updates`.yellow);
log(
this.isJsonOutput,
`- '${e}' was not installed via npm, so we could not check ` +
`for updates`.yellow
);
} else if (err instanceof NoUpdatesAvailableError) {
log(this.isJsonOutput, `- '${e}' had no updates available`.yellow);
} else {
@@ -436,8 +524,15 @@ class ExtensionCommand {
// this is a helper method, 'ext' is assumed to already be installed here, and of the npm
// install type
const {version, pkgName} = this.config.installedExtensions[ext];
let unsafeUpdate = await npm.getLatestVersion(this.config.appiumHome, pkgName);
let safeUpdate = await npm.getLatestSafeUpgradeVersion(this.config.appiumHome, pkgName, version);
let unsafeUpdate = await npm.getLatestVersion(
this.config.appiumHome,
pkgName
);
let safeUpdate = await npm.getLatestSafeUpgradeVersion(
this.config.appiumHome,
pkgName,
version
);
if (!util.compareVersions(unsafeUpdate, '>', version)) {
// the latest version is not greater than the current version, so there's no possible update
unsafeUpdate = null;
@@ -458,16 +553,20 @@ class ExtensionCommand {
* Actually update an extension installed by NPM, using the NPM cli. And update the installation
* manifest.
*
* @param {string} ext - name of extension to update
* @param {string} installSpec - name of extension to update
* @param {string} version - version string identifier to update extension to
* @returns {Promise<void>}
*/
async updateExtension (ext, version) {
const {pkgName} = this.config.installedExtensions[ext];
await fs.rimraf(this.config.getInstallPath(ext));
const extData = await this.installViaNpm({ext, pkgName, pkgVer: version});
delete extData[/** @type {string} */(`${this.type}Name`)];
await this.config.updateExtension(ext, extData);
async updateExtension (installSpec, version) {
const {pkgName} = this.config.installedExtensions[installSpec];
await fs.rimraf(this.config.getInstallPath(installSpec));
const extData = await this.installViaNpm({
installSpec,
pkgName,
pkgVer: version,
});
delete extData[/** @type {string} */ (`${this.type}Name`)];
await this.config.updateExtension(installSpec, extData);
}
/**
@@ -481,33 +580,37 @@ class ExtensionCommand {
* @param {RunOptions} opts
* @return {Promise<RunOutput>}
*/
async _run ({ext, scriptName}) {
if (!_.has(this.config.installedExtensions, ext)) {
async _run ({installSpec, scriptName}) {
if (!_.has(this.config.installedExtensions, installSpec)) {
throw new Error(`please install the ${this.type} first`);
}
const extConfig = this.config.installedExtensions[ext];
const extConfig = this.config.installedExtensions[installSpec];
// note: TS cannot understand that _.has() is a type guard
if (!extConfig.scripts) {
throw new Error(`The ${this.type} named '${ext}' does not contain the ` +
`"scripts" field underneath the "appium" field in its package.json`);
throw new Error(
`The ${this.type} named '${installSpec}' does not contain the ` +
`"scripts" field underneath the "appium" field in its package.json`
);
}
const extScripts = extConfig.scripts;
if (!_.isPlainObject(extScripts)) {
throw new Error(`The ${this.type} named '${ext}' "scripts" field must be a plain object`);
throw new Error(
`The ${this.type} named '${installSpec}' "scripts" field must be a plain object`
);
}
if (!_.has(extScripts, scriptName)) {
throw new Error(`The ${this.type} named '${ext}' does not support the script: '${scriptName}'`);
throw new Error(
`The ${this.type} named '${installSpec}' does not support the script: '${scriptName}'`
);
}
const runner = new SubProcess(process.execPath, [
extScripts[scriptName]
], {
cwd: this.config.getInstallPath(ext)
const runner = new SubProcess(process.execPath, [extScripts[scriptName]], {
cwd: this.config.getInstallPath(installSpec),
});
const output = new RingBuffer(50);
@@ -524,7 +627,10 @@ class ExtensionCommand {
log(this.isJsonOutput, `${scriptName} successfully ran`.green);
return {output: output.getBuff()};
} catch (err) {
log(this.isJsonOutput, `Encountered an error when running '${scriptName}': ${err.message}`.red);
log(
this.isJsonOutput,
`Encountered an error when running '${scriptName}': ${err.message}`.red
);
return {error: err.message, output: output.getBuff()};
}
}
@@ -602,7 +708,7 @@ export {ExtensionCommand};
/**
* Options for {@linkcode ExtensionCommand._run}.
* @typedef RunOptions
* @property {string} ext - name of the extension to run a script from
* @property {string} installSpec - name of the extension to run a script from
* @property {string} scriptName - name of the script to run
*/
@@ -617,7 +723,7 @@ export {ExtensionCommand};
/**
* Options for {@linkcode ExtensionCommand._update}.
* @typedef ExtensionUpdateOpts
* @property {string} ext - the name of the extension to update
* @property {string} installSpec - the name of the extension to update
* @property {boolean} unsafe - if true, will perform unsafe updates past major revision boundaries
*/
@@ -638,7 +744,7 @@ export {ExtensionCommand};
/**
* Options for {@linkcode ExtensionCommand._uninstall}.
* @typedef UninstallOpts
* @property {string} ext - the name or spec of an extension to uninstall
* @property {string} installSpec - the name or spec of an extension to uninstall
*/
/**
@@ -651,7 +757,7 @@ export {ExtensionCommand};
/**
* Options for {@linkcode ExtensionCommand.installViaNpm}
* @typedef InstallViaNpmArgs
* @property {string} ext - the name or spec of an extension to install
* @property {string} installSpec - the name or spec of an extension to install
* @property {string} pkgName - the NPM package name of the extension
* @property {string} [pkgVer] - the specific version of the NPM package
*/
@@ -667,7 +773,7 @@ export {ExtensionCommand};
/**
* Options for {@linkcode ExtensionCommand._install}
* @typedef InstallArgs
* @property {string} ext - the name or spec of an extension to install
* @property {string} installSpec - the name or spec of an extension to install
* @property {import('../../types/appium-manifest').InstallType} installType - how to install this extension. One of the INSTALL_TYPES
* @property {string} [packageName] - for git/github installs, the extension node package name
*/
+33 -18
View File
@@ -1,14 +1,16 @@
import _ from 'lodash';
import ExtensionCommand from './extension-command';
import { KNOWN_PLUGINS } from '../constants';
import {KNOWN_PLUGINS} from '../constants';
const REQ_PLUGIN_FIELDS = ['pluginName', 'mainClass'];
/**
* @extends {ExtensionCommand<PluginType>}
*/
export default class PluginCommand extends ExtensionCommand {
/**
*
* @param {PluginCommandOptions} opts
* @param {import('./extension-command').ExtensionCommandOptions<PluginType>} opts
*/
constructor ({config, json}) {
super({config, json});
@@ -16,41 +18,54 @@ export default class PluginCommand extends ExtensionCommand {
}
async install ({plugin, installType, packageName}) {
return await super._install({ext: plugin, installType, packageName});
return await super._install({
installSpec: plugin,
installType,
packageName,
});
}
async uninstall ({plugin}) {
return await super._uninstall({ext: plugin});
return await super._uninstall({installSpec: plugin});
}
async update ({plugin, unsafe}) {
return await super._update({ext: plugin, unsafe});
return await super._update({installSpec: plugin, unsafe});
}
async run ({plugin, scriptName}) {
return await super._run({ext: plugin, scriptName});
return await super._run({installSpec: plugin, scriptName});
}
getPostInstallText ({extName, extData}) {
return `Plugin ${extName}@${extData.version} successfully installed`.green;
}
validateExtensionFields (appiumPkgData) {
const missingFields = REQ_PLUGIN_FIELDS.reduce((acc, field) => (
appiumPkgData[field] ? acc : [...acc, field]
), []);
/**
* Validates fields in `appium` field of `driverMetadata`
*
* For any `package.json` fields which a driver requires, validate the type of
* those fields on the `package.json` data, throwing an error if anything is
* amiss.
* @param {import('appium/types').ExtMetadata<PluginType>} pluginMetadata
* @param {string} installSpec
* @returns {void}
*/
validateExtensionFields (pluginMetadata, installSpec) {
const missingFields = REQ_PLUGIN_FIELDS.reduce(
(acc, field) => (pluginMetadata[field] ? acc : [...acc, field]),
[]
);
if (!_.isEmpty(missingFields)) {
throw new Error(`Installed plugin did not expose correct fields for compability ` +
`with Appium. Missing fields: ${JSON.stringify(missingFields)}`);
throw new Error(
`Installed plugin "${installSpec}" did not expose correct fields for compability ` +
`with Appium. Missing fields: ${JSON.stringify(missingFields)}`
);
}
}
}
/**
* @typedef PluginCommandOptions
* @property {import('../extension/extension-config').ExtensionConfig<import('../extension/manifest').PluginType>} config
* @property {boolean} json
* @typedef {import('appium/types').PluginType} PluginType
*/
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "appium",
"version": "2.0.0-beta.34",
"version": "2.0.0-beta.35",
"description": "Automation for Apps.",
"keywords": [
"automation",
+1 -1
View File
@@ -315,7 +315,7 @@ describe('CLI behavior', function () {
'--source',
'npm',
]);
await installLocalExtension(appiumHome, 'driver', testDriverPath);
await installLocalExtension(appiumHome, DRIVER_TYPE, testDriverPath);
const list = await runList(['--installed']);
expect(list.fake).to.exist;
expect(list.test).to.exist;
@@ -1,5 +1,6 @@
// @ts-check
import { DRIVER_TYPE } from '../../lib/constants';
import { readConfigFile, normalizeConfig } from '../../lib/config-file';
import { finalizeSchema, registerSchema, resetSchema } from '../../lib/schema/schema';
import extSchema from '../fixtures/driver.schema.js';
@@ -216,7 +217,7 @@ describe('config file behavior', function () {
beforeEach(function () {
resetSchema();
registerSchema('driver', 'fake', extSchema);
registerSchema(DRIVER_TYPE, 'fake', extSchema);
finalizeSchema();
});
+8 -5
View File
@@ -6,9 +6,10 @@
import _ from 'lodash';
import path from 'path';
import { exec } from 'teen_process';
import { PACKAGE_ROOT, resolveFixture } from '../helpers';
import { fs } from '@appium/support';
import {exec} from 'teen_process';
import {PACKAGE_ROOT, resolveFixture} from '../helpers';
import {fs, logger} from '@appium/support';
import stripAnsi from 'strip-ansi';
/**
* Path to the (compiled) main script of the `appium` executable.
@@ -17,6 +18,8 @@ import { fs } from '@appium/support';
*/
export const EXECUTABLE = path.join(PACKAGE_ROOT, 'build', 'lib', 'main.js');
const log = logger.getLogger('appium-e2e-helpers');
/**
* Runs the `appium` executable with the given args.
*
@@ -35,10 +38,11 @@ async function run (appiumHome, args, opts = {}) {
};
try {
args = [...process.execArgv, '--', EXECUTABLE, ...args];
log.debug(stripAnsi(`Running: ${process.execPath} ${args.join(' ')}`));
return await exec(process.execPath, args, {
cwd,
env,
...opts
...opts,
});
} catch (err) {
const {stdout, stderr} = /** @type {TeenProcessExecError} */ (err);
@@ -215,7 +219,6 @@ export function formatAppiumArgErrorOutput (stderr) {
* @returns {Promise<Result>}
*/
/**
* @template [Result=unknown]
* @template {import('../../types/cli').CliExtensionSubcommand} [ExtSubcommand=never]
+2 -1
View File
@@ -1,6 +1,7 @@
// @ts-check
import { fs, tempDir } from '@appium/support';
import path from 'path';
import { DRIVER_TYPE } from '../../lib/constants';
import { resolveFixture } from '../helpers';
import { installLocalExtension, runAppium } from './e2e-helpers';
@@ -29,7 +30,7 @@ describe('CLI behavior controlled by schema', function () {
before(async function () {
await installLocalExtension(
appiumHome,
'driver',
DRIVER_TYPE,
path.dirname(resolveFixture('test-driver/package.json'))
);
help = await runAppium(appiumHome, ['server', '--help']);
@@ -1,5 +1,6 @@
// @ts-check
import { PLUGIN_TYPE } from '../../lib/constants';
import B from 'bluebird';
import { BaseDriver } from '@appium/base-driver';
import { FakeDriver } from '@appium/fake-driver';
@@ -183,7 +184,6 @@ describe('AppiumDriver', function () {
...W3C_CAPS,
alwaysMatch: {
...W3C_CAPS.alwaysMatch,
// @ts-expect-error
'appium:someOtherParm': 'someOtherParm',
},
};
@@ -358,7 +358,7 @@ describe('AppiumDriver', function () {
// to establish defaults, we need to register a schema for the plugin.
// note that the `noargs` plugin does not need a schema, because it
// accepts no arguments.
registerSchema('plugin', ArgsPlugin.pluginName, {
registerSchema(PLUGIN_TYPE, ArgsPlugin.pluginName, {
type: 'object',
properties: {
randomArg: {
@@ -367,7 +367,7 @@ describe('AppiumDriver', function () {
}
}
});
registerSchema('plugin', ArrayArgPlugin.pluginName, {
registerSchema(PLUGIN_TYPE, ArrayArgPlugin.pluginName, {
type: 'object',
properties: {
arr: {
+3 -2
View File
@@ -4,8 +4,9 @@ import _ from 'lodash';
import { createSandbox } from 'sinon';
import { getParser } from '../../lib/cli/parser';
import { checkNodeOk, getBuildInfo, getNonDefaultServerArgs, showBuildInfo, showConfig, validateTmpDir, warnNodeDeprecations } from '../../lib/config';
import { PLUGIN_TYPE } from '../../lib/constants';
import logger from '../../lib/logger';
import { getDefaultsForSchema, resetSchema, registerSchema, finalizeSchema } from '../../lib/schema/schema';
import { finalizeSchema, getDefaultsForSchema, registerSchema, resetSchema } from '../../lib/schema/schema';
describe('Config', function () {
/** @type {sinon.SinonSandbox} */
@@ -187,7 +188,7 @@ describe('Config', function () {
describe('with extension schemas', function () {
beforeEach(function () {
resetSchema();
registerSchema('plugin', 'crypto-fiend', {type: 'object', properties: {elite: {type: 'boolean', default: true}}});
registerSchema(PLUGIN_TYPE, 'crypto-fiend', {type: 'object', properties: {elite: {type: 'boolean', default: true}}});
finalizeSchema();
getParser(true);
args = getDefaultsForSchema();
@@ -315,7 +315,7 @@ describe('Manifest', function () {
/** @type {ExtData<DriverType>} */
beforeEach(function () {
manifest.addExtension('driver', 'foo', extData);
manifest.addExtension(DRIVER_TYPE, 'foo', extData);
});
it('should rewrite', function () {
@@ -412,7 +412,7 @@ describe('Manifest', function () {
packageJson,
'/some/path/to/package.json',
);
expect(manifest.getExtensionData('plugin')).to.deep.equal({
expect(manifest.getExtensionData(PLUGIN_TYPE)).to.deep.equal({
myPlugin: {
mainClass: 'SomeClass',
pkgName: 'derp',
@@ -504,7 +504,7 @@ describe('Manifest', function () {
it('should add a found extension', async function () {
await manifest.syncWithInstalledExtensions();
expect(manifest.getExtensionData('driver')).to.have.property(
expect(manifest.getExtensionData(DRIVER_TYPE)).to.have.property(
'myDriver',
);
});
+35 -35
View File
@@ -1,4 +1,4 @@
import { DRIVER_TYPE, PLUGIN_TYPE } from '../../lib/constants';
import { getParser } from '../../lib/cli/parser';
import { INSTALL_TYPES } from '../../lib/extension/extension-config';
import * as schema from '../../lib/schema/schema';
@@ -21,7 +21,7 @@ describe('parser', function () {
it('should accept only server and driver subcommands', function () {
p.parseArgs([]);
p.parseArgs(['server']);
p.parseArgs(['driver', 'list']);
p.parseArgs([DRIVER_TYPE, 'list']);
(() => p.parseArgs(['foo'])).should.throw();
(() => p.parseArgs(['foo --bar'])).should.throw();
});
@@ -136,7 +136,7 @@ describe('parser', function () {
// we have to require() here because babel will not compile stuff in node_modules
// (even if it's in the monorepo; there may be a way around this)
// anyway, if we do that, we need to use the `default` prop.
schema.registerSchema('driver', 'fake', require('@appium/fake-driver/build/lib/fake-driver-schema').default);
schema.registerSchema(DRIVER_TYPE, 'fake', require('@appium/fake-driver/build/lib/fake-driver-schema').default);
schema.finalizeSchema();
p = getParser(true);
});
@@ -160,12 +160,12 @@ describe('parser', function () {
it('should not yet apply defaults', function () {
const args = p.parseArgs([]);
args.should.not.have.property('driver');
args.should.not.have.property(DRIVER_TYPE);
});
it('should nicely handle extensions w/ dashes in them', function () {
schema.resetSchema();
schema.registerSchema('plugin', 'crypto-fiend', {type: 'object', properties: {elite: {type: 'boolean'}}});
schema.registerSchema(PLUGIN_TYPE, 'crypto-fiend', {type: 'object', properties: {elite: {type: 'boolean'}}});
schema.finalizeSchema();
p = getParser(true);
const args = p.parseArgs([
@@ -198,123 +198,123 @@ describe('parser', function () {
p = getParser(true);
});
it('should not allow random sub-subcommands', function () {
(() => p.parseArgs(['driver', 'foo'])).should.throw();
(() => p.parseArgs([DRIVER_TYPE, 'foo'])).should.throw();
});
describe('list', function () {
it('should allow an empty argument list', function () {
const args = p.parseArgs(['driver', 'list']);
args.subcommand.should.eql('driver');
const args = p.parseArgs([DRIVER_TYPE, 'list']);
args.subcommand.should.eql(DRIVER_TYPE);
args.driverCommand.should.eql('list');
args.showInstalled.should.eql(false);
args.showUpdates.should.eql(false);
args.json.should.eql(false);
});
it('should allow json format', function () {
const args = p.parseArgs(['driver', 'list', '--json']);
const args = p.parseArgs([DRIVER_TYPE, 'list', '--json']);
args.json.should.eql(true);
});
it('should allow --installed', function () {
const args = p.parseArgs(['driver', 'list', '--installed']);
const args = p.parseArgs([DRIVER_TYPE, 'list', '--installed']);
args.showInstalled.should.eql(true);
});
it('should allow --updates', function () {
const args = p.parseArgs(['driver', 'list', '--updates']);
const args = p.parseArgs([DRIVER_TYPE, 'list', '--updates']);
args.showUpdates.should.eql(true);
});
});
describe('install', function () {
it('should not allow an empty argument list', function () {
(() => p.parseArgs(['driver', 'install'])).should.throw();
(() => p.parseArgs([DRIVER_TYPE, 'install'])).should.throw();
});
it('should take a driver name to install', function () {
const args = p.parseArgs(['driver', 'install', 'foobar']);
args.subcommand.should.eql('driver');
const args = p.parseArgs([DRIVER_TYPE, 'install', 'foobar']);
args.subcommand.should.eql(DRIVER_TYPE);
args.driverCommand.should.eql('install');
args.driver.should.eql('foobar');
should.not.exist(args.installType);
args.json.should.eql(false);
});
it('should allow json format', function () {
const args = p.parseArgs(['driver', 'install', 'foobar', '--json']);
const args = p.parseArgs([DRIVER_TYPE, 'install', 'foobar', '--json']);
args.json.should.eql(true);
});
it('should allow --source', function () {
for (const source of INSTALL_TYPES) {
const args = p.parseArgs(['driver', 'install', 'foobar', '--source', source]);
const args = p.parseArgs([DRIVER_TYPE, 'install', 'foobar', '--source', source]);
args.installType.should.eql(source);
}
});
it('should not allow unknown --source', function () {
(() => p.parseArgs(['driver', 'install', 'fobar', '--source', 'blah'])).should.throw();
(() => p.parseArgs([DRIVER_TYPE, 'install', 'fobar', '--source', 'blah'])).should.throw();
});
});
describe('uninstall', function () {
it('should not allow an empty argument list', function () {
(() => p.parseArgs(['driver', 'uninstall'])).should.throw();
(() => p.parseArgs([DRIVER_TYPE, 'uninstall'])).should.throw();
});
it('should take a driver name to uninstall', function () {
const args = p.parseArgs(['driver', 'uninstall', 'foobar']);
args.subcommand.should.eql('driver');
const args = p.parseArgs([DRIVER_TYPE, 'uninstall', 'foobar']);
args.subcommand.should.eql(DRIVER_TYPE);
args.driverCommand.should.eql('uninstall');
args.driver.should.eql('foobar');
args.json.should.eql(false);
});
it('should allow json format', function () {
const args = p.parseArgs(['driver', 'uninstall', 'foobar', '--json']);
const args = p.parseArgs([DRIVER_TYPE, 'uninstall', 'foobar', '--json']);
args.json.should.eql(true);
});
});
describe('update', function () {
it('should not allow an empty argument list', function () {
(() => p.parseArgs(['driver', 'update'])).should.throw();
(() => p.parseArgs([DRIVER_TYPE, 'update'])).should.throw();
});
it('should take a driver name to update', function () {
const args = p.parseArgs(['driver', 'update', 'foobar']);
args.subcommand.should.eql('driver');
const args = p.parseArgs([DRIVER_TYPE, 'update', 'foobar']);
args.subcommand.should.eql(DRIVER_TYPE);
args.driverCommand.should.eql('update');
args.driver.should.eql('foobar');
args.json.should.eql(false);
});
it('should allow json format', function () {
const args = p.parseArgs(['driver', 'update', 'foobar', '--json']);
const args = p.parseArgs([DRIVER_TYPE, 'update', 'foobar', '--json']);
args.json.should.eql(true);
});
});
describe('run', function () {
it('should not allow an empty driver argument list', function () {
(() => p.parseArgs(['driver', 'run'])).should.throw();
(() => p.parseArgs([DRIVER_TYPE, 'run'])).should.throw();
});
it('should not allow no driver scriptName', function () {
(() => p.parseArgs(['driver', 'run', 'foo'])).should.throw();
(() => p.parseArgs([DRIVER_TYPE, 'run', 'foo'])).should.throw();
});
it('should take a driverName and scriptName to run', function () {
const args = p.parseArgs(['driver', 'run', 'foo', 'bar']);
args.subcommand.should.eql('driver');
const args = p.parseArgs([DRIVER_TYPE, 'run', 'foo', 'bar']);
args.subcommand.should.eql(DRIVER_TYPE);
args.driverCommand.should.eql('run');
args.driver.should.eql('foo');
args.scriptName.should.eql('bar');
args.json.should.eql(false);
});
it('should allow json format for driver', function () {
const args = p.parseArgs(['driver', 'run', 'foo', 'bar', '--json']);
const args = p.parseArgs([DRIVER_TYPE, 'run', 'foo', 'bar', '--json']);
args.json.should.eql(true);
});
it('should not allow an empty plugin argument list', function () {
(() => p.parseArgs(['plugin', 'run'])).should.throw();
(() => p.parseArgs([PLUGIN_TYPE, 'run'])).should.throw();
});
it('should not allow no plugin scriptName', function () {
(() => p.parseArgs(['plugin', 'run', 'foo'])).should.throw();
(() => p.parseArgs([PLUGIN_TYPE, 'run', 'foo'])).should.throw();
});
it('should take a pluginName and scriptName to run', function () {
const args = p.parseArgs(['plugin', 'run', 'foo', 'bar']);
args.subcommand.should.eql('plugin');
const args = p.parseArgs([PLUGIN_TYPE, 'run', 'foo', 'bar']);
args.subcommand.should.eql(PLUGIN_TYPE);
args.pluginCommand.should.eql('run');
args.plugin.should.eql('foo');
args.scriptName.should.eql('bar');
args.json.should.eql(false);
});
it('should allow json format for plugin', function () {
const args = p.parseArgs(['plugin', 'run', 'foo', 'bar', '--json']);
const args = p.parseArgs([PLUGIN_TYPE, 'run', 'foo', 'bar', '--json']);
args.json.should.eql(true);
});
});
@@ -1,6 +1,7 @@
// @ts-check
import {ArgSpec} from '../../../lib/schema/arg-spec';
import { DRIVER_TYPE } from '../../../lib/constants';
import { ArgSpec } from '../../../lib/schema/arg-spec';
const {expect} = chai;
@@ -23,7 +24,7 @@ describe('ArgSpec', function () {
describe('when provided extension information', function () {
it('should return a schema ID for a specific argument within an extension schema', function () {
expect(ArgSpec.toSchemaRef('bar', 'driver', 'stuff')).to.equal(
expect(ArgSpec.toSchemaRef('bar', DRIVER_TYPE, 'stuff')).to.equal(
'driver-stuff.json#/properties/bar',
);
});
@@ -41,7 +42,7 @@ describe('ArgSpec', function () {
describe('when provided extension information', function () {
it('should return a schema ID for an extension', function () {
expect(ArgSpec.toSchemaBaseRef('driver', 'stuff')).to.equal(
expect(ArgSpec.toSchemaBaseRef(DRIVER_TYPE, 'stuff')).to.equal(
'driver-stuff.json',
);
});
@@ -57,7 +58,7 @@ describe('ArgSpec', function () {
describe('when provided extension information', function () {
it('should return an extension-specific arg name', function () {
expect(ArgSpec.toArg('no-oats', 'driver', 'bad-donkey')).to.equal(
expect(ArgSpec.toArg('no-oats', DRIVER_TYPE, 'bad-donkey')).to.equal(
'driver-bad-donkey-no-oats',
);
});
@@ -75,7 +76,7 @@ describe('ArgSpec', function () {
describe('when provided the schema ID of an extension schema', function () {
expect(
ArgSpec.extensionInfoFromRootSchemaId('driver-stuff.json'),
).to.eql({extType: 'driver', normalizedExtName: 'stuff'});
).to.eql({extType: DRIVER_TYPE, normalizedExtName: 'stuff'});
});
});
});
@@ -1,9 +1,10 @@
// @ts-check
import _ from 'lodash';
import {resetSchema, finalizeSchema, registerSchema} from '../../../lib/schema';
import {toParserArgs} from '../../../lib/schema/cli-args';
import {transformers} from '../../../lib/schema/cli-transformers';
import { PLUGIN_TYPE } from '../../../lib/constants';
import { finalizeSchema, registerSchema, resetSchema } from '../../../lib/schema';
import { toParserArgs } from '../../../lib/schema/cli-args';
import { transformers } from '../../../lib/schema/cli-transformers';
const {expect} = chai;
@@ -29,7 +30,7 @@ describe('cli-args', function () {
describe('schema contents', function () {
const extName = 'blob';
const extType = 'plugin';
const extType = PLUGIN_TYPE;
describe('type', function () {
let result;
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.16.19](https://github.com/appium/appium/compare/@appium/doctor@1.16.18...@appium/doctor@1.16.19) (2022-05-03)
**Note:** Version bump only for package @appium/doctor
## [1.16.18](https://github.com/appium/appium/compare/@appium/doctor@1.16.17...@appium/doctor@1.16.18) (2022-05-02)
**Note:** Version bump only for package @appium/doctor
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/doctor",
"version": "1.16.18",
"version": "1.16.19",
"description": "Test environment for fitness to run Appium",
"keywords": [
"automation",
@@ -48,7 +48,7 @@
"@appium/support": "file:../support",
"@babel/runtime": "7.17.9",
"@colors/colors": "1.5.0",
"appium-adb": "9.2.1",
"appium-adb": "9.2.2",
"bluebird": "3.7.2",
"inquirer": "8.2.4",
"lodash": "4.17.21",
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
+44 -58
View File
@@ -6,18 +6,22 @@ module.exports = {
sourceType: 'module',
ecmaFeatures: {
impliedStrict: true,
experimentalObjectRestSpread: true,
},
experimentalObjectRestSpread: true
}
},
env: {
node: true,
mocha: true,
es6: true,
},
plugins: ['import', 'mocha', 'promise'],
plugins: [
'import',
'mocha',
'promise'
],
globals: {
chai: true,
should: true,
should: true
},
rules: {
'no-console': 2,
@@ -25,31 +29,23 @@ module.exports = {
radix: [2, 'always'],
'dot-notation': 2,
eqeqeq: [2, 'smart'],
'brace-style': [
2,
'1tbs',
{
allowSingleLine: true,
'brace-style': [2, '1tbs', {
allowSingleLine: true,
}],
indent: [2, 2, {
VariableDeclarator: {
var: 2,
let: 2,
const: 3,
},
],
indent: [
2,
2,
{
VariableDeclarator: {
var: 2,
let: 2,
const: 3,
},
ImportDeclaration: 'first',
CallExpression: {
arguments: 'off',
},
MemberExpression: 'off',
ObjectExpression: 'first',
SwitchCase: 1,
ImportDeclaration: 'first',
CallExpression: {
arguments: 'off',
},
],
MemberExpression: 'off',
ObjectExpression: 'first',
SwitchCase: 1,
}],
'comma-dangle': 0,
'no-empty': 0,
'object-shorthand': 2,
@@ -73,43 +69,31 @@ module.exports = {
// enforce spacing
'arrow-spacing': 2,
'keyword-spacing': 2,
'comma-spacing': [
2,
{
before: false,
after: true,
},
],
'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-before-function-paren': [2, 'always'],
'space-unary-ops': [2, {
words: true,
nonwords: false,
}],
'space-infix-ops': 2,
'key-spacing': [
2,
{
mode: 'strict',
beforeColon: false,
afterColon: true,
},
],
'key-spacing': [2, {
mode: 'strict',
beforeColon: false,
afterColon: true,
}],
'no-multi-spaces': 2,
quotes: [
2,
'single',
{
avoidEscape: true,
allowTemplateLiterals: true,
},
],
quotes: [2, 'single', {
avoidEscape: true,
allowTemplateLiterals: true,
}],
'no-buffer-constructor': 1,
'require-atomic-updates': 0,
'no-prototype-builtins': 1,
@@ -129,5 +113,7 @@ module.exports = {
},
],
},
extends: ['eslint:recommended', 'prettier'],
extends: [
'eslint:recommended',
],
};
+8 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/eslint-config-appium",
"version": "5.1.0",
"version": "5.1.1",
"description": "Shared config for Appium's eslint style",
"keywords": [
"eslint",
@@ -8,15 +8,14 @@
"appium",
"es2015"
],
"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"
},
"bugs": {
"url": "https://github.com/appium/appium/issues"
},
"license": "Apache-2.0",
"author": "https://github.com/appium",
"main": "index.js",
@@ -34,17 +33,17 @@
"@babel/core": "7.17.10",
"@babel/eslint-parser": "7.17.0",
"eslint": "7.32.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-mocha": "9.0.0",
"eslint-plugin-promise": "6.0.0"
},
"publishConfig": {
"access": "public"
},
"homepage": "https://appium.io",
"engines": {
"node": ">=12",
"npm": ">=6"
},
"publishConfig": {
"access": "public"
},
"gitHead": "722ae145fb40bf17b0168ea8b025763f120ad574"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [6.0.10](https://github.com/appium/appium/compare/@appium/gulp-plugins@6.0.9...@appium/gulp-plugins@6.0.10) (2022-05-03)
**Note:** Version bump only for package @appium/gulp-plugins
## [6.0.9](https://github.com/appium/appium/compare/@appium/gulp-plugins@6.0.8...@appium/gulp-plugins@6.0.9) (2022-05-02)
**Note:** Version bump only for package @appium/gulp-plugins
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/gulp-plugins",
"version": "6.0.9",
"version": "6.0.10",
"description": "Custom gulp plugins to be used across all appium modules",
"keywords": [
"automation",
@@ -76,7 +76,7 @@
"js-yaml": "4.1.0",
"lodash": "4.17.21",
"merge-stream": "2.0.0",
"mocha": "9.2.2",
"mocha": "10.0.0",
"moment": "2.29.3",
"node-notifier": "10.0.1",
"nyc": "15.1.0",