Compare commits

...
Author SHA1 Message Date
Jonathan Lipps 76df4d600d chore: publish
- appium@2.4.0
 - @appium/base-driver@9.5.0
 - @appium/base-plugin@2.2.26
 - @appium/doctor@2.0.31
 - @appium/docutils@1.0.2
 - @appium/driver-test-support@0.4.20
 - @appium/execute-driver-plugin@3.0.23
 - @appium/fake-driver@5.4.0
 - @appium/fake-plugin@3.1.30
 - @appium/images-plugin@3.0.1
 - @appium/opencv@3.0.1
 - @appium/plugin-test-support@0.3.27
 - @appium/support@4.2.0
 - @appium/test-support@3.0.30
 - @appium/types@0.16.0
2024-01-03 13:51:42 -08:00
Mykola Mokhnach eff6d331f2 fix(support): Appium version comparison (#19602) 2024-01-03 22:45:25 +01:00
Edgars Eglītis 5070a34739 docs: update READMEs (#19595)
* docs: update universal-xml-plugin readme

* chore: fix line lengths

* docs: update relaxed-caps-plugin readme

* docs: update images-plugin readme

* docs: update fake-plugin readme

* docs: add readme for execute-driver-plugin

* docs: update base-plugin readme

* docs: update test-support readme

* docs: update support readme

* docs: update opencv readme

* docs: update fake-driver readme

* docs: update docutils readme

* docs: update doctor readme

* docs: update base-driver readme

* docs: fix badge for base-driver

* docs: slightly improve base-driver/plugin readme

* docs: fix title for eslint-config-appium-ts

* docs: add badges for non-prerelease packages

* docs: update appium readme

* docs: update some readme links

* chore: address review comments
2024-01-02 21:37:30 +02:00
renovate[bot] bd88198a9a chore(deps): update dependency tsd to v0.30.2 (#19599)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-02 14:23:22 +02:00
renovate[bot] 8e83996eb6 chore(deps): update eslint-related packages to v6.17.0 2024-01-02 11:04:27 +00:00
renovate[bot] 122a4ec111 chore(deps): update dependency @types/node to v20.10.6 2023-12-30 09:27:57 +00:00
renovate[bot] 94b358022f fix(types): update dependency type-fest to v4 (#19592)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-28 15:21:21 +02:00
renovate[bot] 34654956a3 fix(doctor): update dependency appium-adb to v11.0.9 2023-12-28 10:50:25 +00:00
renovate[bot] ea8394a888 fix(docutils): update dependency lilconfig to v3 (#19489)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-27 19:45:41 +02:00
renovate[bot] b11366c3fc fix(support): update dependency moment to v2.30.1 2023-12-27 16:48:47 +00:00
renovate[bot] 38d9f92d1a chore(deps): update dependency tsd to v0.30.1 (#19584)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-27 11:00:04 +01:00
renovate[bot] fe5245a427 fix(appium): update dependency ws to v8.16.0 (#19586)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-27 10:59:48 +01:00
renovate[bot] e77aa417a0 fix(support): update dependency moment to v2.30.0 (#19587)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-27 10:59:34 +01:00
Mykola Mokhnach 7de3c1d57a chore(support): Deprecate AppiumLogger.errorAndThrow() (#19577) 2023-12-27 10:59:15 +01:00
renovate[bot] 441b2848da fix(support): update dependency axios to v1.6.3 2023-12-27 09:10:45 +00:00
renovate[bot] 523cf6fa59 chore(deps): update eslint-related packages to v6.16.0 2023-12-26 09:31:27 +00:00
renovate[bot] f5fce7ced9 chore(docutils): update dependency mkdocs-material to v9.5.3 2023-12-24 10:48:56 +00:00
renovate[bot] a304a1f78e fix(base-driver): update dependency async-lock to v1.4.1 2023-12-23 09:35:18 +00:00
renovate[bot] 88ab563fa0 chore(support): update dependency sharp to v0.33.1 (#19547)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-23 01:02:54 +01:00
Kazuaki Matsuo 923a40b5eb chore(appium): use ~ instead of ^ to strict versions a bit (#19576) 2023-12-22 15:46:23 -08:00
Mykola Mokhnach 73bf68cc60 feat: Deny install/upgrade of packages which server dep does not meet the current Appium version (#19575) 2023-12-22 22:41:30 +01:00
Kazuaki Matsuo 8c4646f3f3 appium(support): remove "--prefer-offline" to avoid possible installation error (#19571) 2023-12-22 08:47:42 -08:00
renovate[bot] 92d1172984 fix(appium): update dependency ws to v8.15.1 2023-12-22 09:23:15 +00:00
Mykola Mokhnach dde74d98b6 chore(appium): Ditch the redundant find-up dependency (#19570) 2023-12-21 23:32:38 +01:00
Edgars Eglītis 96c19c7887 chore(support): update dependency sharp to v0.33.0 (#19569) 2023-12-21 22:47:00 +02:00
Jonathan Lipps ac9b2bd4fc test(appium): ignore lint of mkdocs generated js 2023-12-21 12:32:15 -08:00
Jonathan Lipps f88d68f295 test(appium): wrap bluebird config in error handling for tests 2023-12-21 12:32:15 -08:00
Jonathan Lipps 2b21e66891 feat: add webdriver bidi support 2023-12-21 12:32:15 -08:00
renovate[bot] 9b06b871eb fix(execute-driver-plugin): update dependency webdriverio to v8.27.0 2023-12-21 09:02:23 +00:00
Mykola Mokhnach 893b9e15d6 feat(appium,support): Add common shortcuts for doctor checks (#19562) 2023-12-21 09:24:36 +01:00
Mykola Mokhnach 9aae933581 chore(appium): Tune the error message for a non-existing extension (#19561) 2023-12-21 09:06:15 +01:00
Mykola Mokhnach dc2cedfecd feat(appium): Print the list of available extension scripts if no script name is provided (#19539) 2023-12-20 14:09:48 +01:00
Mykola Mokhnach a30286b6e7 feat(appium): Make doctor extensible (#19542) 2023-12-19 18:40:04 +01:00
renovate[bot] 3bdb8a4f4a chore(deps): update eslint-related packages to v6.15.0 2023-12-19 17:16:57 +00:00
renovate[bot] ff767b7f25 fix(execute-driver-plugin): update dependency webdriverio to v8.26.3 2023-12-19 12:21:15 +00:00
renovate[bot] cbf5e1e6de chore(docutils): update dependency mkdocs-git-revision-date-localized-plugin to v1.2.2 2023-12-19 09:27:48 +00:00
Edgars Eglītis e41935fd64 chore: Update CHANGELOG.md 2023-12-18 15:38:31 -08:00
Edgars Eglītis 73837f1063 chore: Update CHANGELOG.md 2023-12-18 15:38:31 -08:00
Edgars Eglītis b114030e94 chore(docutils): update changelog
Due to how lerna works, it is not straightforward to bump a package from 0.x to 1.x, so breaking changes for this package were labeled as fixes. This adjusts the changelog to add more appropriate labels for the changes.
2023-12-18 15:38:31 -08:00
107 changed files with 3213 additions and 1528 deletions
+1
View File
@@ -6,3 +6,4 @@ examples/javascript-wd
sample-code
**/build-fixtures/**
packages/appium/docs/**/assets/**
packages/appium/docs/**/js/**
+4
View File
@@ -45,6 +45,10 @@ on:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
prepare_matrix:
runs-on: ubuntu-latest
+2
View File
@@ -29,6 +29,8 @@ build/
.idea
*DerivedData
__pycache__
certificate.cert
certificate.key
# csharp
*.pyc
+130 -159
View File
@@ -1,214 +1,185 @@
## Appium
[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://vshymanskyy.github.io/StandWithUkraine/)
<p align="center">
<a href="https://appium.io/">
<img alt="Appium" src="https://raw.githubusercontent.com/appium/appium/master/packages/appium/docs/assets/images/appium-logo-horiz.png" width="500">
</a>
</p>
<p align="center">
Cross-platform test automation for native, hybrid, mobile web and desktop apps.
</p>
[![NPM version](https://badge.fury.io/js/appium.svg)](https://npmjs.org/package/appium)
[![Monthly Downloads](https://img.shields.io/npm/dm/appium.svg)](https://npmjs.org/package/appium)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fappium%2Fappium.svg?type=shield)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fappium%2Fappium?ref=badge_shield)
[![StandWithUkraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://github.com/vshymanskyy/StandWithUkraine/)
[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://vshymanskyy.github.io/StandWithUkraine/)
***
Appium is an open-source, cross-platform test automation tool for native,
hybrid, mobile web and desktop apps. Initially created to automate iOS and Android mobile
applications Appium has grown to a full-featured platform that provides [WebDriver](https://www.w3.org/TR/webdriver/)-based automation possibilities for the whole set of different mobile and desktop platforms.
See the full list of
[officially-supported](https://appium.io/docs/en/latest/ecosystem/#drivers) and
[third-party](https://appium.io/docs/en/latest/ecosystem/#other-drivers) drivers for more details.
<p align="center"><b>
<a href="https://appium.io">Documentation</a> |
<a href="https://appium.io/docs/en/latest/intro/">Get Started</a> |
<a href="https://appium.io/docs/en/latest/ecosystem/">Ecosystem</a> |
<a href="https://github.com/appium/appium/blob/master/packages/appium/CHANGELOG.md">Changelog</a> |
<a href="https://github.com/appium/appium/blob/master/ROADMAP.md">Roadmap</a> |
<a href="https://discuss.appium.io">Discussion Forum</a>
</b></p>
:bangbang: Appium core team does not maintain Appium 1.x anymore since the 1st of January 2022. All recent versions of officially supported platform drivers are not compatible to Appium 1.x, and require Appium 2 to run. [Please read the migration guide from 1.x to 2.0](https://appium.io/docs/en/latest/guides/migrating-1-to-2/) to manage the Appium server.
***
:bangbang: If you still use the deprecated Appium Desktop containing the built-in server then you won't be able to upgrade to Appium 2. Consider switching to [Appium Inspector](https://github.com/appium/appium-inspector) instead.
Appium is an open-source automation framework that provides
[WebDriver](https://www.w3.org/TR/webdriver/)-based automation possibilities for a wide range of
different mobile, desktop and IoT platforms. Appium is modular and extensible, and supports multiple
programming languages, which means there is an entire ecosystem of related software:
* [__Drivers__](#drivers) add support for automating specific platforms
* [__Clients__](#clients) allow writing Appium tests in your programming language of choice
* [__Plugins__](#plugins) allow to further extend Appium functionality
### Documentation
## Migrating From Appium 1 to Appium 2
The current Appium documentation is available at [__https://appium.io__](https://appium.io).
As of January 1st, 2022, the Appium team no longer maintains or supports Appium 1. All officially
supported platform drivers are only compatible with Appium 2.
### Requirements
__[Please read the Migration Guide](https://appium.io/docs/en/latest/guides/migrating-1-to-2/) if you
are still using Appium 1.__
- macOS, Linux or Windows operating system
- Node.js 16+. Any relevant [LTS release](https://nodejs.dev/en/about/releases/) should work just fine.
- NPM (Node Package Manager) 8+
> [!WARNING]
> If you use Appium Desktop or Appium Server GUI, you will not be able to upgrade to Appium 2, as
both of these tools have been deprecated. Please use [Appium Inspector](https://github.com/appium/appium-inspector)
in combination with a standalone Appium 2 server.
These are only server requirements. Each driver might have its own requirements. Consider checking the corresponding driver tutorial for more details.
## Installation
### Server
Appium can be installed using `npm` (other package managers are not currently supported). Please
check the [installation docs](http://appium.io/docs/en/latest/quickstart/install/) for the
system requirements and further information.
To install Appium 2 server using Node Package Manager (npm) run the following command:
If upgrading from Appium 1, make sure Appium 1 is fully uninstalled (`npm uninstall -g appium`).
Unexpected errors might appear if this has not been done.
```bash
npm i -g appium
```
To update Appium 2 server using Node Package Manager (npm) run the following command:
Note that this will only install the core Appium server, which cannot automate anything on its own.
Please install [drivers](#drivers) for your target platforms in order to automate them.
```bash
npm i -g appium@latest
```
## Drivers
:bangbang: Make sure Appium 1 is fully uninstalled before upgrading it to Appium 2 (`npm uninstall -g appium`). Unexpected errors might appear if this has not been done.
Appium supports app automation across a variety of platforms, like iOS, Android, macOS, Windows,
and more. Each platform is supported by one or more "drivers", which know how to automate that
particular platform. You can find a full list of officially-supported and third-party drivers in
[Appium Ecosystem's Drivers page](http://appium.io/docs/en/latest/ecosystem/drivers/).
### Drivers
Appium supports app automation across a variety of platforms, like iOS,
Android, and Windows. Each platform is supported by one or more "drivers",
which know how to automate that particular platform. Since version 2.0
all drivers have been isolated from the Appium server app and can
be managed independently using the [appium driver](https://appiumpro.com/editions/122-installing-appium-20-and-the-driver-and-plugins-cli) command line interface.
In general, the drivers management in Appium 2 is as simple as:
Driver management is done using [Appium's Extension command-line interface](http://appium.io/docs/en/latest/cli/extensions/):
```bash
# To install a new driver from npm
appium driver install --source=npm appium-xcuitest-driver[@<version>]
# To install a driver from a local folder (useful for developers)
appium driver install --source=local /Users/me/sources/appium-xcuitest-driver
# To install a new driver from github (hm, maybe it's time to publish it to NPM?)
appium driver install --source=github --package=appium-xcuitest-driver appium/appium-xcuitest-driver
# Install an official driver from npm (see documentation for a list of such drivers)
appium driver install <driver-name>
# Install any driver from npm
appium driver install --source=npm <driver-name>
# See documentation for installation from other sources
# To list already installed drivers
# List already installed drivers
appium driver list --installed
# To update a driver (it must be already installed)
appium driver update xcuitest
# To uninstall a driver (it won't last forever, wouldn't it?)
appium driver uninstall xcuitest
# Update a driver (it must be already installed)
# This will NOT update the major version, in order to prevent breaking changes
appium driver update <driver-name>
# Update a driver to the most recent version (may include breaking changes)
appium driver update <driver-name> --unsafe
# Uninstall a driver (it won't last forever, will it?)
appium driver uninstall <driver-name>
```
You can find a full list of
[officially-supported](https://appium.io/docs/en/latest/ecosystem/#drivers) and
[third-party](https://appium.io/docs/en/latest/ecosystem/#other-drivers) drivers at
the current Appium 2.0 documentation.
## Clients
### Plugins
Client libraries enable writing Appium tests in different programming languages. There are
officially-supported clients for Java, Python, Ruby, and .NET C#, as well as third-party clients
for other languages. You can find a full list of clients in
[Appium Ecosystem's Clients page](http://appium.io/docs/en/latest/ecosystem/clients/).
The concept of plugins is something new that has been added exclusively to Appium2. Plugins allow you to extend server functionality without changing the server code. Plugins could be managed similarly to drivers:
## Plugins
Plugins allow you to extend server functionality without changing the server code. The main
difference between drivers and plugins is that the latter must be explicitly enabled on
Appium server startup (all installed drivers are enabled by default):
```bash
# To install an officially supported plugin
appium plugin install images
# To install a plugin from a local folder (useful for developers)
appium plugin install --source=local /Users/me/sources/images
# To install a new plugin from npm
appium plugin install --source=npm appium-device-farm
appium --use-plugins=<plugin-name>
```
# To list already installed plugins
You can find a full list of officially-supported and third-party plugins in
[Appium Ecosystem's Plugins page](http://appium.io/docs/en/latest/ecosystem/plugins/).
Similarly to drivers, plugin management is also done using
[Appium's Extension command-line interface](http://appium.io/docs/en/latest/cli/extensions/):
```bash
# Install an official plugin from npm (see documentation for a list of such plugins)
appium plugin install <plugin-name>
# Install any plugin from npm
appium plugin install --source=npm <plugin-name>
# See documentation for installation from other sources
# List already installed plugins
appium plugin list --installed
# To update a plugins (it must be already installed)
appium plugin update appium-device-farm
# To uninstall a plugin
appium plugin uninstall appium-device-farm
# Update a plugin (it must be already installed)
# This will NOT update the major version, in order to prevent breaking changes
appium plugin update <plugin-name>
# Update a plugin to the most recent version (may include breaking changes)
appium plugin update <plugin-name> --unsafe
# Uninstall a plugin
appium plugin uninstall <plugin-name>
```
The main difference between drivers and plugins is that the latter must be explicitly enabled on server startup after it was installed (drivers are enabled by default after installation):
## Server Command Line Interface
In order to start sending commands to the Appium server, it must be running on the URL and port
where your client library expects it to listen. [Appium's command-line interface](http://appium.io/docs/en/latest/cli/args/)
is used to launch and configure the server:
```bash
appium server --use-plugins=device-farm,images
```
You can find a full list of
[officially-supported](https://appium.io/docs/en/latest/ecosystem/#plugins) and
[third-party](https://appium.io/docs/en/latest/ecosystem/#other-plugins) plugins at
the current Appium 2.0 documentation.
### Server Command Line Interface
In order to start sending commands to Appium over the wire it must be listening
on the URL where your client library expects it to listen.
Use the following commands to run and configure Appium server:
```bash
# Start the server on the default port and host (e.g. http://0.0.0.0:4723/)
# Start the server on the default host (0.0.0.0) and port (4723)
appium server
# Start the server on the given port, host and use the base path prefix (the default prefix is /)
appium server -p 9000 -a 127.0.0.1 -pa /wd/hub
# Get the list of all supported command line parameters.
# This list would also include descriptions of driver-specific
# command line arguments for all installed drivers.
# Each driver and plugin must have their command line arguments
# exposed in a special JSON schema declared as a part of the corresponding
# package.json file.
appium server --help
# You can also omit the 'server' subcommand
appium
# Start the server on the given host, port and use a custom base path prefix (the default prefix is '/')
appium --address 127.0.0.1 --port 9000 --base-path /wd/hub
```
Appium supports execution of parallel server processes as well as parallel driver sessions within
Appium supports execution of parallel server processes, as well as parallel driver sessions within a
single server process. Refer the corresponding driver documentations regarding which mode is optimal
for the particular driver or whether it supports parallel sessions.
### Why Appium?
## Why Appium?
1. You usually don't have to recompile your app or modify it in any way, due
to the use of standard automation APIs on all platforms.
2. You can write tests with your favorite dev tools using any
[WebDriver](https://w3c.github.io/webdriver/webdriver-spec.html)-compatible
language such as [Java](https://github.com/appium/java-client),
[JavaScript](https://webdriver.io/), [Python](https://github.com/appium/python-client),
[Ruby](https://github.com/appium/ruby_lib), [C#](https://github.com/appium/dotnet-client)
with the Selenium WebDriver API. There are also various third party
client implementations for other languages.
1. You usually don't have to recompile your app or modify it in any way, due to the use of standard
automation APIs on all platforms.
2. You can write tests with your favorite dev tools using any WebDriver-compatible language such as
Java, Python, Ruby and C#. There are also third party client implementations for other languages.
3. You can use any testing framework.
4. Some drivers, like xcuitest and uiautomator2 ones have built-in mobile web and
hybrid app support. Within the same script, you can switch seamlessly between native
app automation and webview automation, all using the WebDriver model that's already
the standard for web automation.
5. You can run your automated tests locally and in a cloud. There are multiple
cloud providers that support various Appium drivers (mostly
targeting iOS and Android mobile automation).
6. [Appium Inspector](https://github.com/appium/appium-inspector) allows
visual debugging of automated tests and could be extremely useful for
beginners.
4. Some drivers like `xcuitest` and `uiautomator2` have built-in mobile web and hybrid app support.
Within the same script, you can switch seamlessly between native app automation and webview
automation, all using the WebDriver model that's already the standard for web automation.
5. You can run your automated tests locally and in a cloud. There are multiple cloud providers that
support various Appium drivers (mostly targeting iOS and Android mobile automation).
6. [Appium Inspector](https://github.com/appium/appium-inspector) can be used to visually inspect
the page source of applications across different platforms, facilitating easier test development.
Investing in the
[WebDriver](https://w3c.github.io/webdriver/webdriver-spec.html) protocol means
you are betting on a single, free, and open protocol for testing that has become
a web standard. Don't lock yourself into a proprietary stack.
Investing in the [WebDriver](https://w3c.github.io/webdriver/webdriver-spec.html) protocol means you
are betting on a single, free, and open protocol for testing that has become a web standard. Don't
lock yourself into a proprietary stack.
For example, if you use Apple's XCUITest library without Appium you can only
write tests using Obj-C/Swift, and you can only run tests through Xcode.
Similarly, with Google's UiAutomator or Espresso, you can only write tests in
Java/Kotlin. Appium opens up the possibility of true cross-platform native app
automation, for mobile and beyond. Finally!
For example, if you use Apple's XCUITest library without Appium, you can only write tests using
Obj-C/Swift, and you can only run tests through Xcode. Similarly, with Google's UiAutomator or
Espresso, you can only write tests in Java/Kotlin. Appium opens up the possibility of true
cross-platform native app automation, for mobile and beyond!
If you're new to Appium or want a more comprehensive description of what this is all
about, please read our [Intro to Appium](https://appium.io/docs/en/latest/intro/).
If you are looking for a more comprehensive description of what this is all about, please read our
documentation on [How Does Appium Work?](https://appium.io/docs/en/latest/intro/appium/).
### Quickstart
## License
Check out our [Quickstart](https://appium.io/docs/en/latest/quickstart/) guide
to get going with Appium.
There is also a sample code that contains [many examples of tests in a variety
of different languages](https://github.com/appium/appium/tree/1.x/sample-code)!
### Documentation
For prettily-rendered docs, please visit [Appium Documentation](https://appium.io).
You can always find the full list of Appium doc pages at [Appium's GitHub
Repo](https://github.com/appium/appium/tree/master/packages/appium/docs) as well.
### Contributing
Please take a look at our [contribution documentation](CONTRIBUTING.md)
for instructions on how to build, test, and run Appium from the source.
### Project History, Credits & Inspiration
* [History](https://appium.io/docs/en/latest/intro/history/)
### User Forums
Announcements and debates often take place on the [Discussion Group](https://discuss.appium.io),
be sure to sign up!
### Troubleshooting
Please find the troubleshooting guide at the corresponding driver repository/documentation.
### License
[Apache-2.0](./LICENSE)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fappium%2Fappium.svg?type=large)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fappium%2Fappium?ref=badge_large)
+560 -764
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -91,7 +91,7 @@
"@types/chai-as-promised": "7.1.8",
"@types/diff": "5.0.9",
"@types/mocha": "10.0.6",
"@types/node": "20.10.5",
"@types/node": "20.10.6",
"@types/semver": "7.5.6",
"@types/sinon": "17.0.2",
"@types/sinon-chai": "3.2.12",
@@ -99,8 +99,8 @@
"@types/teen_process": "2.0.4",
"@types/ws": "8.5.10",
"@types/yargs": "17.0.32",
"@typescript-eslint/eslint-plugin": "6.14.0",
"@typescript-eslint/parser": "6.14.0",
"@typescript-eslint/eslint-plugin": "6.17.0",
"@typescript-eslint/parser": "6.17.0",
"asyncbox": "3.0.0",
"chai": "4.3.10",
"chai-as-promised": "7.1.1",
@@ -137,11 +137,11 @@
"sync-monorepo-packages": "1.0.2",
"through2": "4.0.2",
"ts-node": "10.9.2",
"tsd": "0.30.0",
"tsd": "0.30.2",
"typescript": "5.2.2",
"validate.js": "0.13.1",
"webdriverio": "8.26.2",
"ws": "8.15.1",
"webdriverio": "8.27.0",
"ws": "8.16.0",
"yaml-js": "0.3.1"
},
"engines": {
+25
View File
@@ -3,6 +3,31 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.4.0](https://github.com/appium/appium/compare/appium@2.3.0...appium@2.4.0) (2024-01-03)
### Features
* add webdriver bidi support ([2b21e66](https://github.com/appium/appium/commit/2b21e66891e8ab8c3929f04f32e94eb4efdba691))
* **appium,support:** Add common shortcuts for doctor checks ([#19562](https://github.com/appium/appium/issues/19562)) ([893b9e1](https://github.com/appium/appium/commit/893b9e15d64d2356ea339596e805543df51fa505))
* **appium:** Make doctor extensible ([#19542](https://github.com/appium/appium/issues/19542)) ([a30286b](https://github.com/appium/appium/commit/a30286b6e7b9753f73e55a8f9db14211b3124578))
* **appium:** Print the list of available extension scripts if no script name is provided ([#19539](https://github.com/appium/appium/issues/19539)) ([dc2cedf](https://github.com/appium/appium/commit/dc2cedfecd19581321f6947ef1c847065ba2d3d6))
* Deny install/upgrade of packages which server dep does not meet the current Appium version ([#19575](https://github.com/appium/appium/issues/19575)) ([73bf68c](https://github.com/appium/appium/commit/73bf68cc602de0999e1956110be225c3fd9087c2))
### Bug Fixes
* **appium:** update dependency ws to v8.15.1 ([92d1172](https://github.com/appium/appium/commit/92d117298442e72ae262f224733068ddf147f4c8))
* **appium:** update dependency ws to v8.16.0 ([#19586](https://github.com/appium/appium/issues/19586)) ([fe5245a](https://github.com/appium/appium/commit/fe5245a42784181a9cd7a8ff6b095a622ae40920))
* **base-driver:** update dependency async-lock to v1.4.1 ([a304a1f](https://github.com/appium/appium/commit/a304a1f78e658f6f70cbe8e1efd6d06b81d8d34e))
* **docutils:** update dependency lilconfig to v3 ([#19489](https://github.com/appium/appium/issues/19489)) ([ea8394a](https://github.com/appium/appium/commit/ea8394a888c835bc54f7f68174a4b6cae85339ea))
* **execute-driver-plugin:** update dependency webdriverio to v8.26.3 ([ff767b7](https://github.com/appium/appium/commit/ff767b7f25e97ec9643f691b57ca6ba159dcdcf9))
* **execute-driver-plugin:** update dependency webdriverio to v8.27.0 ([9b06b87](https://github.com/appium/appium/commit/9b06b871eb3d9c60428130442ea7ab433ce4df31))
* **support:** update dependency axios to v1.6.3 ([441b284](https://github.com/appium/appium/commit/441b2848dae28472356f37fc5d51ac27af7bbe29))
* **types:** update dependency type-fest to v4 ([#19592](https://github.com/appium/appium/issues/19592)) ([94b3580](https://github.com/appium/appium/commit/94b358022fdba3050ef94c1f881895f07e24fb75))
## [2.3.0](https://github.com/appium/appium/compare/appium@2.2.3...appium@2.3.0) (2023-12-18)
+130 -159
View File
@@ -1,214 +1,185 @@
## Appium
[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://vshymanskyy.github.io/StandWithUkraine/)
<p align="center">
<a href="https://appium.io/">
<img alt="Appium" src="https://raw.githubusercontent.com/appium/appium/master/packages/appium/docs/assets/images/appium-logo-horiz.png" width="500">
</a>
</p>
<p align="center">
Cross-platform test automation for native, hybrid, mobile web and desktop apps.
</p>
[![NPM version](https://badge.fury.io/js/appium.svg)](https://npmjs.org/package/appium)
[![Monthly Downloads](https://img.shields.io/npm/dm/appium.svg)](https://npmjs.org/package/appium)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fappium%2Fappium.svg?type=shield)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fappium%2Fappium?ref=badge_shield)
[![StandWithUkraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://github.com/vshymanskyy/StandWithUkraine/)
[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://vshymanskyy.github.io/StandWithUkraine/)
***
Appium is an open-source, cross-platform test automation tool for native,
hybrid, mobile web and desktop apps. Initially created to automate iOS and Android mobile
applications Appium has grown to a full-featured platform that provides [WebDriver](https://www.w3.org/TR/webdriver/)-based automation possibilities for the whole set of different mobile and desktop platforms.
See the full list of
[officially-supported](https://appium.io/docs/en/latest/ecosystem/#drivers) and
[third-party](https://appium.io/docs/en/latest/ecosystem/#other-drivers) drivers for more details.
<p align="center"><b>
<a href="https://appium.io">Documentation</a> |
<a href="https://appium.io/docs/en/latest/intro/">Get Started</a> |
<a href="https://appium.io/docs/en/latest/ecosystem/">Ecosystem</a> |
<a href="https://github.com/appium/appium/blob/master/packages/appium/CHANGELOG.md">Changelog</a> |
<a href="https://github.com/appium/appium/blob/master/ROADMAP.md">Roadmap</a> |
<a href="https://discuss.appium.io">Discussion Forum</a>
</b></p>
:bangbang: Appium core team does not maintain Appium 1.x anymore since the 1st of January 2022. All recent versions of officially supported platform drivers are not compatible to Appium 1.x, and require Appium 2 to run. [Please read the migration guide from 1.x to 2.0](https://appium.io/docs/en/latest/guides/migrating-1-to-2/) to manage the Appium server.
***
:bangbang: If you still use the deprecated Appium Desktop containing the built-in server then you won't be able to upgrade to Appium 2. Consider switching to [Appium Inspector](https://github.com/appium/appium-inspector) instead.
Appium is an open-source automation framework that provides
[WebDriver](https://www.w3.org/TR/webdriver/)-based automation possibilities for a wide range of
different mobile, desktop and IoT platforms. Appium is modular and extensible, and supports multiple
programming languages, which means there is an entire ecosystem of related software:
* [__Drivers__](#drivers) add support for automating specific platforms
* [__Clients__](#clients) allow writing Appium tests in your programming language of choice
* [__Plugins__](#plugins) allow to further extend Appium functionality
### Documentation
## Migrating From Appium 1 to Appium 2
The current Appium documentation is available at [__https://appium.io__](https://appium.io).
As of January 1st, 2022, the Appium team no longer maintains or supports Appium 1. All officially
supported platform drivers are only compatible with Appium 2.
### Requirements
__[Please read the Migration Guide](https://appium.io/docs/en/latest/guides/migrating-1-to-2/) if you
are still using Appium 1.__
- macOS, Linux or Windows operating system
- Node.js 16+. Any relevant [LTS release](https://nodejs.dev/en/about/releases/) should work just fine.
- NPM (Node Package Manager) 8+
> [!WARNING]
> If you use Appium Desktop or Appium Server GUI, you will not be able to upgrade to Appium 2, as
both of these tools have been deprecated. Please use [Appium Inspector](https://github.com/appium/appium-inspector)
in combination with a standalone Appium 2 server.
These are only server requirements. Each driver might have its own requirements. Consider checking the corresponding driver tutorial for more details.
## Installation
### Server
Appium can be installed using `npm` (other package managers are not currently supported). Please
check the [installation docs](http://appium.io/docs/en/latest/quickstart/install/) for the
system requirements and further information.
To install Appium 2 server using Node Package Manager (npm) run the following command:
If upgrading from Appium 1, make sure Appium 1 is fully uninstalled (`npm uninstall -g appium`).
Unexpected errors might appear if this has not been done.
```bash
npm i -g appium
```
To update Appium 2 server using Node Package Manager (npm) run the following command:
Note that this will only install the core Appium server, which cannot automate anything on its own.
Please install [drivers](#drivers) for your target platforms in order to automate them.
```bash
npm i -g appium@latest
```
## Drivers
:bangbang: Make sure Appium 1 is fully uninstalled before upgrading it to Appium 2 (`npm uninstall -g appium`). Unexpected errors might appear if this has not been done.
Appium supports app automation across a variety of platforms, like iOS, Android, macOS, Windows,
and more. Each platform is supported by one or more "drivers", which know how to automate that
particular platform. You can find a full list of officially-supported and third-party drivers in
[Appium Ecosystem's Drivers page](http://appium.io/docs/en/latest/ecosystem/drivers/).
### Drivers
Appium supports app automation across a variety of platforms, like iOS,
Android, and Windows. Each platform is supported by one or more "drivers",
which know how to automate that particular platform. Since version 2.0
all drivers have been isolated from the Appium server app and can
be managed independently using the [appium driver](https://appiumpro.com/editions/122-installing-appium-20-and-the-driver-and-plugins-cli) command line interface.
In general, the drivers management in Appium 2 is as simple as:
Driver management is done using [Appium's Extension command-line interface](http://appium.io/docs/en/latest/cli/extensions/):
```bash
# To install a new driver from npm
appium driver install --source=npm appium-xcuitest-driver[@<version>]
# To install a driver from a local folder (useful for developers)
appium driver install --source=local /Users/me/sources/appium-xcuitest-driver
# To install a new driver from github (hm, maybe it's time to publish it to NPM?)
appium driver install --source=github --package=appium-xcuitest-driver appium/appium-xcuitest-driver
# Install an official driver from npm (see documentation for a list of such drivers)
appium driver install <driver-name>
# Install any driver from npm
appium driver install --source=npm <driver-name>
# See documentation for installation from other sources
# To list already installed drivers
# List already installed drivers
appium driver list --installed
# To update a driver (it must be already installed)
appium driver update xcuitest
# To uninstall a driver (it won't last forever, wouldn't it?)
appium driver uninstall xcuitest
# Update a driver (it must be already installed)
# This will NOT update the major version, in order to prevent breaking changes
appium driver update <driver-name>
# Update a driver to the most recent version (may include breaking changes)
appium driver update <driver-name> --unsafe
# Uninstall a driver (it won't last forever, will it?)
appium driver uninstall <driver-name>
```
You can find a full list of
[officially-supported](https://appium.io/docs/en/latest/ecosystem/#drivers) and
[third-party](https://appium.io/docs/en/latest/ecosystem/#other-drivers) drivers at
the current Appium 2.0 documentation.
## Clients
### Plugins
Client libraries enable writing Appium tests in different programming languages. There are
officially-supported clients for Java, Python, Ruby, and .NET C#, as well as third-party clients
for other languages. You can find a full list of clients in
[Appium Ecosystem's Clients page](http://appium.io/docs/en/latest/ecosystem/clients/).
The concept of plugins is something new that has been added exclusively to Appium2. Plugins allow you to extend server functionality without changing the server code. Plugins could be managed similarly to drivers:
## Plugins
Plugins allow you to extend server functionality without changing the server code. The main
difference between drivers and plugins is that the latter must be explicitly enabled on
Appium server startup (all installed drivers are enabled by default):
```bash
# To install an officially supported plugin
appium plugin install images
# To install a plugin from a local folder (useful for developers)
appium plugin install --source=local /Users/me/sources/images
# To install a new plugin from npm
appium plugin install --source=npm appium-device-farm
appium --use-plugins=<plugin-name>
```
# To list already installed plugins
You can find a full list of officially-supported and third-party plugins in
[Appium Ecosystem's Plugins page](http://appium.io/docs/en/latest/ecosystem/plugins/).
Similarly to drivers, plugin management is also done using
[Appium's Extension command-line interface](http://appium.io/docs/en/latest/cli/extensions/):
```bash
# Install an official plugin from npm (see documentation for a list of such plugins)
appium plugin install <plugin-name>
# Install any plugin from npm
appium plugin install --source=npm <plugin-name>
# See documentation for installation from other sources
# List already installed plugins
appium plugin list --installed
# To update a plugins (it must be already installed)
appium plugin update appium-device-farm
# To uninstall a plugin
appium plugin uninstall appium-device-farm
# Update a plugin (it must be already installed)
# This will NOT update the major version, in order to prevent breaking changes
appium plugin update <plugin-name>
# Update a plugin to the most recent version (may include breaking changes)
appium plugin update <plugin-name> --unsafe
# Uninstall a plugin
appium plugin uninstall <plugin-name>
```
The main difference between drivers and plugins is that the latter must be explicitly enabled on server startup after it was installed (drivers are enabled by default after installation):
## Server Command Line Interface
In order to start sending commands to the Appium server, it must be running on the URL and port
where your client library expects it to listen. [Appium's command-line interface](http://appium.io/docs/en/latest/cli/args/)
is used to launch and configure the server:
```bash
appium server --use-plugins=device-farm,images
```
You can find a full list of
[officially-supported](https://appium.io/docs/en/latest/ecosystem/#plugins) and
[third-party](https://appium.io/docs/en/latest/ecosystem/#other-plugins) plugins at
the current Appium 2.0 documentation.
### Server Command Line Interface
In order to start sending commands to Appium over the wire it must be listening
on the URL where your client library expects it to listen.
Use the following commands to run and configure Appium server:
```bash
# Start the server on the default port and host (e.g. http://0.0.0.0:4723/)
# Start the server on the default host (0.0.0.0) and port (4723)
appium server
# Start the server on the given port, host and use the base path prefix (the default prefix is /)
appium server -p 9000 -a 127.0.0.1 -pa /wd/hub
# Get the list of all supported command line parameters.
# This list would also include descriptions of driver-specific
# command line arguments for all installed drivers.
# Each driver and plugin must have their command line arguments
# exposed in a special JSON schema declared as a part of the corresponding
# package.json file.
appium server --help
# You can also omit the 'server' subcommand
appium
# Start the server on the given host, port and use a custom base path prefix (the default prefix is '/')
appium --address 127.0.0.1 --port 9000 --base-path /wd/hub
```
Appium supports execution of parallel server processes as well as parallel driver sessions within
Appium supports execution of parallel server processes, as well as parallel driver sessions within a
single server process. Refer the corresponding driver documentations regarding which mode is optimal
for the particular driver or whether it supports parallel sessions.
### Why Appium?
## Why Appium?
1. You usually don't have to recompile your app or modify it in any way, due
to the use of standard automation APIs on all platforms.
2. You can write tests with your favorite dev tools using any
[WebDriver](https://w3c.github.io/webdriver/webdriver-spec.html)-compatible
language such as [Java](https://github.com/appium/java-client),
[JavaScript](https://webdriver.io/), [Python](https://github.com/appium/python-client),
[Ruby](https://github.com/appium/ruby_lib), [C#](https://github.com/appium/dotnet-client)
with the Selenium WebDriver API. There are also various third party
client implementations for other languages.
1. You usually don't have to recompile your app or modify it in any way, due to the use of standard
automation APIs on all platforms.
2. You can write tests with your favorite dev tools using any WebDriver-compatible language such as
Java, Python, Ruby and C#. There are also third party client implementations for other languages.
3. You can use any testing framework.
4. Some drivers, like xcuitest and uiautomator2 ones have built-in mobile web and
hybrid app support. Within the same script, you can switch seamlessly between native
app automation and webview automation, all using the WebDriver model that's already
the standard for web automation.
5. You can run your automated tests locally and in a cloud. There are multiple
cloud providers that support various Appium drivers (mostly
targeting iOS and Android mobile automation).
6. [Appium Inspector](https://github.com/appium/appium-inspector) allows
visual debugging of automated tests and could be extremely useful for
beginners.
4. Some drivers like `xcuitest` and `uiautomator2` have built-in mobile web and hybrid app support.
Within the same script, you can switch seamlessly between native app automation and webview
automation, all using the WebDriver model that's already the standard for web automation.
5. You can run your automated tests locally and in a cloud. There are multiple cloud providers that
support various Appium drivers (mostly targeting iOS and Android mobile automation).
6. [Appium Inspector](https://github.com/appium/appium-inspector) can be used to visually inspect
the page source of applications across different platforms, facilitating easier test development.
Investing in the
[WebDriver](https://w3c.github.io/webdriver/webdriver-spec.html) protocol means
you are betting on a single, free, and open protocol for testing that has become
a web standard. Don't lock yourself into a proprietary stack.
Investing in the [WebDriver](https://w3c.github.io/webdriver/webdriver-spec.html) protocol means you
are betting on a single, free, and open protocol for testing that has become a web standard. Don't
lock yourself into a proprietary stack.
For example, if you use Apple's XCUITest library without Appium you can only
write tests using Obj-C/Swift, and you can only run tests through Xcode.
Similarly, with Google's UiAutomator or Espresso, you can only write tests in
Java/Kotlin. Appium opens up the possibility of true cross-platform native app
automation, for mobile and beyond. Finally!
For example, if you use Apple's XCUITest library without Appium, you can only write tests using
Obj-C/Swift, and you can only run tests through Xcode. Similarly, with Google's UiAutomator or
Espresso, you can only write tests in Java/Kotlin. Appium opens up the possibility of true
cross-platform native app automation, for mobile and beyond!
If you're new to Appium or want a more comprehensive description of what this is all
about, please read our [Intro to Appium](https://appium.io/docs/en/latest/intro/).
If you are looking for a more comprehensive description of what this is all about, please read our
documentation on [How Does Appium Work?](https://appium.io/docs/en/latest/intro/appium/).
### Quickstart
## License
Check out our [Quickstart](https://appium.io/docs/en/latest/quickstart/) guide
to get going with Appium.
There is also a sample code that contains [many examples of tests in a variety
of different languages](https://github.com/appium/appium/tree/1.x/sample-code)!
### Documentation
For prettily-rendered docs, please visit [Appium Documentation](https://appium.io).
You can always find the full list of Appium doc pages at [Appium's GitHub
Repo](https://github.com/appium/appium/tree/master/packages/appium/docs) as well.
### Contributing
Please take a look at our [contribution documentation](CONTRIBUTING.md)
for instructions on how to build, test, and run Appium from the source.
### Project History, Credits & Inspiration
* [History](https://appium.io/docs/en/latest/intro/history/)
### User Forums
Announcements and debates often take place on the [Discussion Group](https://discuss.appium.io),
be sure to sign up!
### Troubleshooting
Please find the troubleshooting guide at the corresponding driver repository/documentation.
### License
[Apache-2.0](./LICENSE)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fappium%2Fappium.svg?type=large)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fappium%2Fappium?ref=badge_large)
+21
View File
@@ -196,3 +196,24 @@ Example (run the `reset` script included with the UiAutomator2 driver):
```
appium driver run uiautomator2 reset
```
### `doctor`
Run doctor checks for the given extension if it has any. Extension authors can include
runnable scripts that assist with driver or plugin preconditions validations. These scripts
must be valid Node.js sources, must be listed in the package manifest, and must export
class instances that comply to the particular requirements.
See the [Building Doctor Checks](../developing/build-doctor-checks.md) tutorial for more
details on these requirements.
Usage:
```
appium <ext-type> doctor <ext-name>
```
Example (run doctor checks for the UiAutomator2 driver):
```
appium driver doctor uiautomator2
```
@@ -0,0 +1,141 @@
---
title: Building Doctor Checks
---
The idea of Appium Doctor is to assist users with driver or plugin preconditions setup. Sometimes such
preconditions might be quite complicated and require non-trivial technical knowledge. Doctor checks,
which are vanilla Node.js class instances written by extension authors, simplify
the setup process by automating diagnostics and possible fixes for the found issues. These checks
might also be interactive to ensure better usage experience.
This tutorial is supposed to be used by plugin or driver authors that would like to help their users
to deal with complicated setup or configuration steps.
## Adding Doctor Checks
### Typing Requirements
The term `Doctor Check` literally describes a single javascript class instance that implements the
[IDoctorCheck interface](https://github.com/appium/appium/blob/master/packages/types/lib/doctor.ts).
The interface defines the following methods and properties:
- `diagnose(): Promise<DoctorCheckResult>`: Contains the code to diagnose a possible issue
- `fix(): Promise<string|null>`: Either fixes the actual problem if `hasAutofix()` returns true or
returns a string description for possible manual fixes. If this method throws an exception named
`FixSkippedError` and `hasAutofix()` returns true then the result of the method invocation
is going to be ignored.
- `hasAutofix(): boolean`: Whether calling `fix()` would resolve the found issue
- `isOptional(): boolean`: Whether the found issue can be ignored and is not a showstopper
- `log: AppiumLogger`: May be used for logging. This property may be assigned
by the instance itself or by the Appium server if it is left unassigned.
The `DoctorCheckResult` object returned by the `diagnose()` method must contain the following properties:
- `ok: boolean`: Whether the diagnosis found no issues
- `optional: boolean`: Whether the diagnosed issue is safe to ignore
- `message: string`: The text message describing the diagnostic result
### Manifest Requirements
A single extension may export multiple Doctor checks to Appium. In order for these checks to be properly
picked up by the server CLI after the corresponding extension is installed they might be listed in the
package .json manifest under the `appium.doctor.checks` section similar to the definition below:
```json
// ...
"appium": {
"driverName": "fake",
"automationName": "Fake",
"platformNames": [
"Fake"
],
"mainClass": "FakeDriver",
"schema": "./build/lib/fake-driver-schema.js",
"scripts": {
"fake-error": "./build/lib/scripts/fake-error.js",
"fake-success": "./build/lib/scripts/fake-success.js",
"fake-stdin": "./build/lib/scripts/fake-stdin.js"
},
"doctor": {
"checks": [
"./doctor/fake1.js",
"./doctor/fake2.js"
// ...
]
}
},
// ...
```
Also, it makes sense to include the [@appium/types](https://www.npmjs.com/package/@appium/types) import
to the package dev dependencies.
### Implementation Example
The below example is a "raw" Node.JS implementation that does not use any transpilation:
```js
const {fs, doctor} = require('@appium/support');
/** @satisfies {import('@appium/types').IDoctorCheck} */
class EnvVarAndPathCheck {
/**
* @param {string} varName
*/
constructor(varName) {
this.varName = varName;
}
async diagnose() {
const varValue = process.env[this.varName];
if (typeof varValue === 'undefined') {
return doctor.nok(`${this.varName} environment variable is NOT set!`);
}
if (await fs.exists(varValue)) {
return doctor.ok(`${this.varName} is set to: ${varValue}`);
}
return doctor.nok(`${this.varName} is set to '${varValue}' but this is NOT a valid path!`);
}
async fix() {
return (
`Make sure the environment variable ${this.varName} is properly configured for the Appium server process`
);
}
hasAutofix() {
return false;
}
isOptional() {
return false;
}
}
const androidHomeCheck = new EnvVarAndPathCheck('ANDROID_HOME');
module.exports = {androidHomeCheck};
/**
* @typedef {import('@appium/types').DoctorCheckResult} CheckResult
*/
```
This file could be saved as `doctor/android-home-check.js` and then added to the package.json manifest
as
```json
// ...
"appium": {
// ...
"doctor": {
"checks": [
"./doctor/android-home-check.js",
]
}
// ...
},
// ...
```
@@ -270,7 +270,7 @@ access the `this.cliArgs` and `this.caps` objects explicitly.
In all cases, the `appium:` capability prefix will have been stripped away by the time you are
accessing values here, for convenience.
### Implement WebDriver commands
### Implement WebDriver classic commands
You handle WebDriver commands by implementing functions in your driver class. Each member of the
WebDriver Protocol, plus the various Appium extensions, has a corresponding function that you
@@ -309,6 +309,21 @@ A few notes:
- if your driver doesn't implement a command, users can still try to access the command, and will
get a `501 Not Yet Implemented` response error.
### Implement WebDriver BiDi commands
[WebDriver BiDi](https://w3c.github.io/webdriver-bidi) is a newer version of the WebDriver spec
which is implemented over Websockets instead of HTTP. As an Appium driver author you can take
advantage of Appium's BiDi support without having to know anything about the BiDi protocol or
Websockets. Implementing handlers for BiDi commands works just the same as implementing handlers
for WebDriver classic commands (described in the previous section). You simply define a method on
your driver of the appropriate name, and it will be called when the BiDi command is requested by
the client. To see which specific names you should use for BiDi commands, have a look at
[bidi-commands.js](https://github.com/appium/appium/blob/master/packages/base-driver/lib/protocol/bidi-commands.js)
Currently, you also need to define a `doesSupportBidi` field on your driver instances, and ensure
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.
### Implement element finding
Element finding is a special command implementation case. You don't actually want to override
@@ -540,6 +555,21 @@ proxyActive() {
With those pieces in play, you won't have to reimplement anything that's already implemented by the
remote endpoint you're proxying to. Appium will take care of all the proxying for you.
### Proxy BiDi commands to another BiDi implementation
All of the above about proxying WebDriver commands is conceptually also valid for proxying BiDi
commands specifically. In order to enable BiDi proxying, you need to:
1. Set the `doesSupportBidi` field on your driver instances to `true`.
1. Implement `get bidiProxyUrl` on your driver. This should return a Websocket URL which is the
address of the upstream socket you want BiDi commands to be proxied to.
The intended pattern here is for you to start a session on the upstream implementation, check
whether it has an active BiDi socket in the returned capabilities (e.g., the `webSocketUrl`
capability), and then to set an internal field to that value, so that it can be returned by `get
bidiProxyUrl`. Once all this is in place, Appium will proxy BiDi commands from the client straight
to the upstream connection.
### Extend the existing protocol with new commands
You may find that the existing commands don't cut it for your driver. If you want to expose
@@ -659,6 +689,29 @@ async onSettingsUpdate(key, value) {
}
```
### Emit BiDi events
With the WebDriver BiDi protocol, clients can subscribe to arbitrary events which can be sent
asynchronously to the client over the BiDi socket connection. As an Appium driver author you don't
need to worry about event subscription. If you want to emit an event with a certain method name and
payload, it's as easy as using the built-in event emitter with the `bidiEvent` event.
As an
example, let's say our driver wants to periodically emit CPU load information. We could define an
event called `system.cpu`, and a payload that looks like `{load: 0.97}` to signify 97% CPU usage.
Whenever we want, our driver can simply call the following code (assuming we have the current load
in `this.currentCpuLoad`):
```js
this.eventEmitter.emit('bidiEvent', {
method: 'system.cpu',
params: {load: this.currentCpuLoad},
})
```
Now, if the client has subscribed to the `system.cpu` event, it will be notified with the load
whenever the driver emits it.
### Make itself aware of resources other concurrent drivers are using
Let's say your driver uses up some system resources, like ports. There are a few ways to make sure
@@ -15,3 +15,4 @@ own Appium extension:
* For creating a driver, see the [Build Drivers](./build-drivers.md) page
* For creating a plugin, take a look at the [Build Plugins](build-plugins.md) page
* Drivers and plugins both need documentation, so check out the [Build Documentation](./build-docs.md) page
* For creating a doctor check, see the [Building Doctor Checks](./build-doctor-checks.md) page
+387 -1
View File
@@ -1,5 +1,6 @@
/* eslint-disable no-unused-vars */
import _ from 'lodash';
import B from 'bluebird';
import {getBuildInfo, updateBuildInfo, APPIUM_VER} from './config';
import {
BaseDriver,
@@ -9,6 +10,7 @@ import {
CREATE_SESSION_COMMAND,
DELETE_SESSION_COMMAND,
GET_STATUS_COMMAND,
MAX_LOG_BODY_LENGTH,
promoteAppiumOptions,
promoteAppiumOptionsForObject,
} from '@appium/base-driver';
@@ -16,7 +18,8 @@ import AsyncLock from 'async-lock';
import {parseCapsForInnerDriver, pullSettings, makeNonW3cCapsError} from './utils';
import {util, node, logger} from '@appium/support';
import {getDefaultsForExtension} from './schema';
import {DRIVER_TYPE} from './constants';
import {DRIVER_TYPE, BIDI_BASE_PATH, BIDI_EVENT_NAME} from './constants';
import WebSocket from 'ws';
const desiredCapabilityConstraints = /** @type {const} */ ({
automationName: {
@@ -83,6 +86,12 @@ class AppiumDriver extends DriverCore {
/** @type {AppiumServer} */
server;
/** @type {Record<string, import('ws').WebSocket[]>} */
bidiSockets;
/** @type {Record<string, import('ws').WebSocket>} */
bidiProxyClients;
/**
* @type {AppiumDriverConstraints}
* @readonly
@@ -113,6 +122,8 @@ class AppiumDriver extends DriverCore {
this.pluginClasses = new Map();
this.sessionPlugins = {};
this.sessionlessPlugins = [];
this.bidiSockets = {};
this.bidiProxyClients = {};
this.desiredCapConstraints = desiredCapabilityConstraints;
this._isShuttingDown = false;
@@ -241,6 +252,337 @@ 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`,
);
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
@@ -410,6 +752,21 @@ class AppiumDriver extends DriverCore {
);
await driverInstance.updateSettings(jwpSettings);
}
// if the user has asked for bidi support, and the driver says that it supports bidi,
// send our bidi url back to the user. The inner driver will need to have already saved any
// internal bidi urls it might want to proxy to, cause we are going to overwrite that
// information here!
if (dCaps.webSocketUrl && driverInstance.doesSupportBidi) {
const {address, port, basePath} = this.args;
const isUsingSsl = this.args.sslCertificatePath && this.args.sslKeyPath;
const scheme = isUsingSsl ? 'wss' : 'ws';
const bidiUrl = `${scheme}://${address}:${port}${basePath}${BIDI_BASE_PATH}/${innerSessionId}`;
// @ts-ignore webSocketUrl gets sent by the client as a boolean, but then it is supposed
// to come back from the server as a string. TODO figure out how to express this in our
// capability constraint system
dCaps.webSocketUrl = bidiUrl;
}
} catch (error) {
return {
protocol,
@@ -515,6 +872,9 @@ class AppiumDriver extends DriverCore {
// be in otherwise
delete this.sessions[sessionId];
delete this.sessionPlugins[sessionId];
this.cleanupBidiSockets(sessionId);
return dstSession;
});
// this may not be correct, but if `dstSession` was falsy, the call to `deleteSession()` would
@@ -535,6 +895,32 @@ class AppiumDriver extends DriverCore {
}
}
/**
* @param {string} sessionId
*/
cleanupBidiSockets(sessionId) {
// clean up any bidi sockets associated with session
if (this.bidiSockets[sessionId]) {
try {
this.log.debug(`Closing bidi socket(s) associated with session ${sessionId}`);
for (const ws of this.bidiSockets[sessionId]) {
// 1001 means server is going away
ws.close(1001, 'Appium session is closing');
}
} catch {}
delete this.bidiSockets[sessionId];
const proxyClient = this.bidiProxyClients[sessionId];
if (proxyClient) {
this.log.debug(`Also closing proxy connection to upstream bidi server`);
try {
// 1000 means normal closure, which seems correct when Appium is acting as the client
proxyClient.close(1000);
} catch {}
delete this.bidiProxyClients[sessionId];
}
}
}
async deleteAllSessions(opts = {}) {
const sessionsCount = _.size(this.sessions);
if (0 === sessionsCount) {
+27 -2
View File
@@ -7,6 +7,7 @@ import {
EXT_SUBCOMMAND_RUN,
EXT_SUBCOMMAND_UNINSTALL,
EXT_SUBCOMMAND_UPDATE,
EXT_SUBCOMMAND_DOCTOR,
} from '../constants';
import {INSTALL_TYPES} from '../extension/extension-config';
import {toParserArgs} from '../schema/cli-args';
@@ -49,6 +50,7 @@ const getExtensionArgs = _.memoize(function getExtensionArgs() {
[EXT_SUBCOMMAND_UNINSTALL]: makeUninstallArgs(type),
[EXT_SUBCOMMAND_UPDATE]: makeUpdateArgs(type),
[EXT_SUBCOMMAND_RUN]: makeRunArgs(type),
[EXT_SUBCOMMAND_DOCTOR]: makeDoctorArgs(type),
};
}
return /** @type {Record<ExtensionType, Record<import('appium/types').CliExtensionSubcommand,ArgumentDefinitions>>} */ (
@@ -156,7 +158,28 @@ function makeUninstallArgs(type) {
{
type: 'str',
help:
'Name of the driver to uninstall, for example: ' + type === DRIVER_TYPE
`Name of the ${type} to uninstall, for example: ` + type === DRIVER_TYPE
? DRIVER_EXAMPLE
: PLUGIN_EXAMPLE,
},
],
]);
}
/**
* Makes the opts for the `doctor` subcommand for each extension type
* @param {ExtensionType} type
* @returns {ArgumentDefinitions}
*/
function makeDoctorArgs(type) {
return new Map([
...globalExtensionArgs,
[
[type],
{
type: 'str',
help:
`Name of the ${type} to run doctor checks for, for example: ` + type === DRIVER_TYPE
? DRIVER_EXAMPLE
: PLUGIN_EXAMPLE,
},
@@ -222,10 +245,12 @@ function makeRunArgs(type) {
['scriptName'],
{
default: null,
nargs: '?',
type: 'str',
help:
`Name of the script to run from the ${type}. The script name must be a key ` +
`inside the "appium.scripts" field inside the ${type}'s "package.json" file`,
`inside the "appium.scripts" field inside the ${type}'s "package.json" file. ` +
`If not provided then all available script names are going to be listed.`,
},
],
]);
+18
View File
@@ -66,6 +66,18 @@ export default class DriverCliCommand extends ExtensionCliCommand {
});
}
/**
* Runs doctor checks for the given driver
*
* @param {DriverDoctorOptions} opts
* @returns {Promise<import('@appium/types').IDoctorCheck[]>}
*/
async doctor({driver}) {
return await super._doctor({
installSpec: driver,
});
}
/**
*
* @param {import('./extension-command').ExtensionArgs} opts
@@ -151,3 +163,9 @@ export default class DriverCliCommand extends ExtensionCliCommand {
* @property {string} scriptName - name of the script to run
* @property {string[]} [extraArgs] - arguments to pass to the script
*/
/**
* Options for {@linkcode DriverCliCommand.doctor}.
* @typedef DriverDoctorOptions
* @property {string} driver - name of the driver to run doctor checks for
*/
+148 -2
View File
@@ -2,7 +2,7 @@
import B from 'bluebird';
import _ from 'lodash';
import path from 'path';
import {npm, util, env, console} from '@appium/support';
import {npm, util, env, console, fs, system} from '@appium/support';
import {spinWith, RingBuffer} from './utils';
import {
INSTALL_TYPE_NPM,
@@ -15,6 +15,10 @@ import {SubProcess} from 'teen_process';
import {packageDidChange} from '../extension/package-changed';
import {spawn} from 'child_process';
import {inspect} from 'node:util';
import {pathToFileURL} from 'url';
import {Doctor} from '../doctor/doctor';
import {npmPackage} from '../utils';
import semver from 'semver';
const UPDATE_ALL = 'installed';
@@ -31,6 +35,19 @@ function receiptToManifest(receipt) {
return /** @type {ExtManifest<ExtType>} */ (_.omit(receipt, 'driverName', 'pluginName'));
}
/**
* Fetches the remote extension version requirement
*
* @param {string} installSpec Extension name (could also contain the version suffix)
* @returns {Promise<[string, string|null]>}
*/
async function getRemoteExtensionVersionReq(installSpec) {
const allDeps = await npm.getPackageInfo(installSpec, ['peerDependencies', 'dependencies']);
const requiredVersionPair = _.flatMap(_.values(allDeps).map(_.toPairs))
.find(([name]) => name === 'appium');
return [npmPackage.version, requiredVersionPair ? requiredVersionPair[1] : null];
}
/**
* @template {ExtensionType} ExtType
*/
@@ -223,6 +240,29 @@ class ExtensionCliCommand {
return listData;
}
/**
* Checks whether the given extension is compatible with the currently installed server
*
* @param {InstallViaNpmArgs} installViaNpmOpts
* @returns {Promise<void>}
*/
async _checkInstallCompatibility({installSpec, pkgName, installType}) {
if (INSTALL_TYPE_NPM !== installType) {
return;
}
await spinWith(this.isJsonOutput, `Checking if '${pkgName}' is compatible`, async () => {
const [serverVersion, extVersionRequirement] = await getRemoteExtensionVersionReq(installSpec);
if (serverVersion && extVersionRequirement && !semver.satisfies(serverVersion, extVersionRequirement)) {
throw this._createFatalError(
`'${installSpec}' cannot be installed because the server version it requires (${extVersionRequirement}) ` +
`does not meet the currently installed one (${serverVersion}). Please install ` +
`a compatible server version first.`
);
}
});
}
/**
* Install an extension
*
@@ -332,6 +372,8 @@ class ExtensionCliCommand {
);
}
await this._checkInstallCompatibility(installViaNpmOpts);
receipt = await this.installViaNpm(installViaNpmOpts);
// this _should_ be the same as `probablyExtName` as the one derived above unless
@@ -695,6 +737,85 @@ class ExtensionCliCommand {
});
}
/**
* Runs doctor checks for the given extension
*
* @param {DoctorOptions} opts
* @returns {Promise<import('@appium/types').IDoctorCheck[]>}
*/
async _doctor({installSpec}) {
if (!this.config.isInstalled(installSpec)) {
throw this._createFatalError(`The ${this.type} "${installSpec}" is not installed`);
}
const moduleRoot = this.config.getInstallPath(installSpec);
const packageJsonPath = path.join(moduleRoot, 'package.json');
if (!await fs.exists(packageJsonPath)) {
throw this._createFatalError(
`No package.json could be found for "${installSpec}" ${this.type}`
);
}
let doctorSpec;
try {
doctorSpec = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')).appium?.doctor;
} catch (e) {
throw this._createFatalError(
`The manifest at '${packageJsonPath}' cannot be parsed: ${e.message}`
);
}
if (!doctorSpec) {
this.log.info(`The ${this.type} "${installSpec}" does not export any doctor checks`);
return [];
}
if (!_.isPlainObject(doctorSpec) || !_.isArray(doctorSpec.checks)) {
throw this._createFatalError(
`The 'doctor' entry in the package manifest '${packageJsonPath}' must be a proper object ` +
`containing the 'checks' key with the array of script paths`
);
}
const paths = doctorSpec.checks.map((/** @type {string} */ p) => {
const scriptPath = path.resolve(moduleRoot, p);
if (!path.normalize(scriptPath).startsWith(path.normalize(moduleRoot))) {
this.log.error(
`The doctor check script '${p}' from the package manifest '${packageJsonPath}' must be located ` +
`in the '${moduleRoot}' root folder. It will be skipped`
);
return null;
}
return scriptPath;
}).filter(Boolean);
/** @type {Promise[]} */
const loadChecksPromises = [];
for (const p of paths) {
const promise = (async () => {
// https://github.com/nodejs/node/issues/31710
const scriptPath = system.isWindows() ? pathToFileURL(p).href : p;
try {
return await import(scriptPath);
} catch (e) {
this.log.warn(`Unable to load doctor checks from '${p}': ${e.message}`);
}
})();
loadChecksPromises.push(promise);
}
const isDoctorCheck = (/** @type {any} */ x) =>
['diagnose', 'fix', 'hasAutofix', 'isOptional'].every((method) => _.isFunction(x?.[method]));
/** @type {import('@appium/types').IDoctorCheck[]} */
const checks = _.flatMap((await B.all(loadChecksPromises)).filter(Boolean).map(_.toPairs))
.map(([, value]) => value)
.filter(isDoctorCheck);
if (_.isEmpty(checks)) {
this.log.info(`The ${this.type} "${installSpec}" exports no valid doctor checks`);
return [];
}
this.log.debug(
`Running ${util.pluralize('doctor check', checks.length, true)} ` +
`for the "${installSpec}" ${this.type}`
);
await new Doctor(checks).run();
return checks;
}
/**
* Runs a script cached inside the `scripts` field under `appium`
* inside of the extension's `package.json` file. Will throw
@@ -729,6 +850,24 @@ class ExtensionCliCommand {
);
}
if (!scriptName) {
const allScripts = _.toPairs(extScripts);
const root = this.config.getInstallPath(installSpec);
const existingScripts = await B.filter(
allScripts,
async ([, p]) => await fs.exists(path.join(root, p))
);
if (_.isEmpty(existingScripts)) {
this.log.info(`The ${this.type} named '${installSpec}' does not contain any scripts`);
} else {
this.log.info(`The ${this.type} named '${installSpec}' contains ` +
`${util.pluralize('script', existingScripts.length, true)}:`);
existingScripts.forEach(([name]) => this.log.info(` - ${name}`));
}
this.log.ok(`Successfully retrieved the list of scripts`.green);
return {};
}
if (!(scriptName in /** @type {Record<string,string>} */ (extScripts))) {
throw this._createFatalError(
`The ${this.type} named '${installSpec}' does not support the script: '${scriptName}'`
@@ -874,11 +1013,18 @@ export {ExtensionCliCommand as ExtensionCommand};
* Options for {@linkcode ExtensionCliCommand._run}.
* @typedef RunOptions
* @property {string} installSpec - name of the extension to run a script from
* @property {string} scriptName - name of the script to run
* @property {string} [scriptName] - name of the script to run. If not provided
* then all available script names will be printed
* @property {string[]} [extraArgs] - arguments to pass to the script
* @property {boolean} [bufferOutput] - if true, will buffer the output of the script and return it
*/
/**
* Options for {@linkcode ExtensionCliCommand.doctor}.
* @typedef DoctorOptions
* @property {string} installSpec - name of the extension to run doctor checks for
*/
/**
* Return value of {@linkcode ExtensionCliCommand._run}
*
+22 -8
View File
@@ -2,7 +2,17 @@ import {fs} from '@appium/support';
import {ArgumentParser} from 'argparse';
import _ from 'lodash';
import path from 'path';
import {DRIVER_TYPE, PLUGIN_TYPE, SERVER_SUBCOMMAND} from '../constants';
import {
DRIVER_TYPE,
EXT_SUBCOMMAND_DOCTOR,
EXT_SUBCOMMAND_INSTALL,
EXT_SUBCOMMAND_LIST,
EXT_SUBCOMMAND_RUN,
EXT_SUBCOMMAND_UNINSTALL,
EXT_SUBCOMMAND_UPDATE,
PLUGIN_TYPE,
SERVER_SUBCOMMAND
} from '../constants';
import {finalizeSchema, getArgSpec, hasArgSpec} from '../schema';
import {rootDir} from '../config';
import {getExtensionArgs, getServerArgs} from './args';
@@ -191,8 +201,7 @@ class ArgParser {
const serverArgs = getServerArgs();
for (const [flagsOrNames, opts] of serverArgs) {
// TS doesn't like the spread operator here.
// @ts-ignore
// @ts-ignore TS doesn't like the spread operator here.
serverParser.add_argument(...flagsOrNames, {...opts});
}
@@ -221,33 +230,38 @@ class ArgParser {
*/
const parserSpecs = [
{
command: 'list',
command: EXT_SUBCOMMAND_LIST,
args: extensionArgs[type].list,
help: `List available and installed ${type}s`,
aliases: ['ls'],
},
{
command: 'install',
command: EXT_SUBCOMMAND_INSTALL,
args: extensionArgs[type].install,
help: `Install a ${type}`,
},
{
command: 'uninstall',
command: EXT_SUBCOMMAND_UNINSTALL,
args: extensionArgs[type].uninstall,
help: `Uninstall a ${type}`,
},
{
command: 'update',
command: EXT_SUBCOMMAND_UPDATE,
args: extensionArgs[type].update,
help: `Update installed ${type}s to the latest version`,
},
{
command: 'run',
command: EXT_SUBCOMMAND_RUN,
args: extensionArgs[type].run,
help:
`Run a script (defined inside the ${type}'s package.json under the ` +
`“scripts” field inside the “appium” field) from an installed ${type}`,
},
{
command: EXT_SUBCOMMAND_DOCTOR,
args: extensionArgs[type].doctor,
help: `Run doctor checks (if any defined) for the given ${type}`,
},
];
for (const {command, args, help, aliases} of parserSpecs) {
+18
View File
@@ -65,6 +65,18 @@ export default class PluginCliCommand extends ExtensionCliCommand {
});
}
/**
* Runs doctor checks for the given plugin
*
* @param {PluginDoctorOptions} opts
* @returns {Promise<import('@appium/types').IDoctorCheck[]>}
*/
async doctor({plugin}) {
return await super._doctor({
installSpec: plugin,
});
}
/**
*
* @param {import('./extension-command').ExtensionArgs} opts
@@ -141,3 +153,9 @@ export default class PluginCliCommand extends ExtensionCliCommand {
* @property {string} scriptName - name of the script to run
* @property {string[]} [extraArgs] - arguments to pass to the script
*/
/**
* Options for {@linkcode PluginCliCommand.doctor}.
* @typedef PluginDoctorOptions
* @property {string} plugin - name of the plugin to run doctor checks for
*/
+5 -7
View File
@@ -2,14 +2,14 @@
import ora from 'ora';
const JSON_SPACES = 4;
export const JSON_SPACES = 4;
/***
* Log an error to the console and exit the process.
* @param {boolean} json - whether we should log json or text
* @param {any} msg - error message, object, Error instance, etc.
*/
function errAndQuit(json, msg) {
export function errAndQuit(json, msg) {
if (json) {
console.log(JSON.stringify({error: `${msg}`}, null, JSON_SPACES));
} else {
@@ -26,7 +26,7 @@ function errAndQuit(json, msg) {
* @param {boolean} json - whether we are in json mode (and should therefore not log)
* @param {string} msg - string to log
*/
function log(json, msg) {
export function log(json, msg) {
!json && console.log(msg);
}
@@ -36,7 +36,7 @@ function log(json, msg) {
* @param {string} msg - string to log
* @param {function} fn - function to wrap with spinning
*/
async function spinWith(json, msg, fn) {
export async function spinWith(json, msg, fn) {
if (json) {
return await fn();
}
@@ -52,7 +52,7 @@ async function spinWith(json, msg, fn) {
}
}
class RingBuffer {
export class RingBuffer {
constructor(size = 50) {
this.size = size;
this.buffer = [];
@@ -70,5 +70,3 @@ class RingBuffer {
this.buffer.push(item);
}
}
export {errAndQuit, log, spinWith, JSON_SPACES, RingBuffer};
+18 -24
View File
@@ -4,17 +4,14 @@ import {system, fs} from '@appium/support';
import axios from 'axios';
import {exec} from 'teen_process';
import semver from 'semver';
import findUp from 'find-up';
import os from 'node:os';
import {npmPackage} from './utils';
import {getDefaultsForSchema, getAllArgSpecs} from './schema/schema';
const npmPackage = fs.readPackageJsonFrom(__dirname);
const APPIUM_VER = npmPackage.version;
export const APPIUM_VER = npmPackage.version;
const ENGINES = /** @type {Record<string,string>} */ (npmPackage.engines);
const MIN_NODE_VERSION = ENGINES.node;
const GIT_META_ROOT = '.git';
const GIT_BINARY = `git${system.isWindows() ? '.exe' : ''}`;
const GITHUB_API = 'https://api.github.com/repos/appium/appium';
@@ -44,27 +41,25 @@ async function updateBuildInfo(useGithubApiFallback = false) {
}
}
/**
* Finds the Git metadata dir (see `GIT_META_ROOT`)
*
* This is needed because Appium cannot assume `package.json` and `.git` are in the same
* directory. Monorepos, see?
* @returns {Promise<string|undefined>} Path to dir or `undefined` if not found
*/
async function findGitRoot() {
return await findUp(GIT_META_ROOT, {cwd: rootDir, type: 'directory'});
}
/** @type {() => Promise<string?>} */
const getFullGitPath = _.memoize(async function getFullGitPath() {
try {
return await fs.which(GIT_BINARY);
} catch {
return null;
}
});
/**
* @param {boolean} [useGithubApiFallback]
* @returns {Promise<string?>}
*/
async function getGitRev(useGithubApiFallback = false) {
const gitRoot = await findGitRoot();
if (gitRoot) {
const fullGitPath = await getFullGitPath();
if (fullGitPath) {
try {
const {stdout} = await exec(GIT_BINARY, ['rev-parse', 'HEAD'], {
cwd: gitRoot,
const {stdout} = await exec(fullGitPath, ['rev-parse', 'HEAD'], {
cwd: __dirname,
});
return stdout.trim();
} catch (ign) {}
@@ -94,11 +89,11 @@ async function getGitRev(useGithubApiFallback = false) {
* @returns {Promise<string?>}
*/
async function getGitTimestamp(commitSha, useGithubApiFallback = false) {
const gitRoot = await findGitRoot();
if (gitRoot) {
const fullGitPath = await getFullGitPath();
if (fullGitPath) {
try {
const {stdout} = await exec(GIT_BINARY, ['show', '-s', '--format=%ci', commitSha], {
cwd: gitRoot,
const {stdout} = await exec(fullGitPath, ['show', '-s', '--format=%ci', commitSha], {
cwd: __dirname,
});
return stdout.trim();
} catch (ign) {}
@@ -321,7 +316,6 @@ export {
showBuildInfo,
getNonDefaultServerArgs,
getGitRev,
APPIUM_VER,
updateBuildInfo,
showConfig,
rootDir,
+12
View File
@@ -64,6 +64,7 @@ export const EXT_SUBCOMMAND_INSTALL = 'install';
export const EXT_SUBCOMMAND_UNINSTALL = 'uninstall';
export const EXT_SUBCOMMAND_UPDATE = 'update';
export const EXT_SUBCOMMAND_RUN = 'run';
export const EXT_SUBCOMMAND_DOCTOR = 'doctor';
/**
* Current revision of the manifest (`extensions.yaml`) schema
@@ -77,3 +78,14 @@ export const CURRENT_SCHEMA_REV = 4;
* memory usage, perf, and/or log output, and higher limits may be difficult to scan.
*/
export const LONG_STACKTRACE_LIMIT = 100;
/**
* Where should the bidi websocket handler live on the server?
*/
export const BIDI_BASE_PATH = '/bidi';
/**
* The name of the event for drivers to emit when they want to send bidi events to a client over
* a bidi socket
*/
export const BIDI_EVENT_NAME = 'bidiEvent';
+209
View File
@@ -0,0 +1,209 @@
import '@colors/colors';
import _ from 'lodash';
import { util, doctor, logger } from '@appium/support';
export class Doctor {
/**
* @param {DoctorCheck[]} [checks=[]]
*/
constructor(checks = []) {
this.log = logger.getLogger('Doctor');
/** @type {DoctorCheck[]} */
this.checks = checks;
this.checks
.filter((c) => _.isNil(c.log))
.forEach((c) => { c.log = this.log; });
/** @type {DoctorIssue[]} */
this.foundIssues = [];
}
/**
* @returns {DoctorIssue[]}
*/
get issuesRequiredToFix() {
return this.foundIssues.filter((f) => !f.check.isOptional());
}
/**
* @returns {DoctorIssue[]}
*/
get issuesOptionalToFix() {
return this.foundIssues.filter((f) => f.check.isOptional());
}
/**
* The doctor shows the report
*/
async diagnose() {
this.log.info(`### Starting doctor diagnostics ###`);
this.foundIssues = [];
for (const check of this.checks) {
const res = await check.diagnose();
const issue = this.toIssue(res, check);
if (issue) {
this.foundIssues.push(issue);
}
}
this.log.info(
`### Diagnostic completed, ${this.buildFixMessage()}. ###`
);
this.log.info('');
}
/**
* @returns {Promise<boolean>}
*/
async reportManualIssues() {
const manualIssues = _.filter(this.issuesRequiredToFix, (f) => !f.check.hasAutofix());
const manualIssuesOptional = _.filter(this.issuesOptionalToFix, (f) => !f.check.hasAutofix());
const handleIssues = async (headerLogs, issues) => {
if (_.isEmpty(issues)) {
return;
}
for (const logMsg of headerLogs) {
this.log.info(logMsg);
}
/** @type {string[]} */
const fixMessages = [];
for (const issue of issues) {
const message = await issue.check.fix();
if (message) {
fixMessages.push(message);
}
}
for (const m of _.uniq(fixMessages)) {
this.log.warn(` \u279C ${m}`);
}
this.log.info('');
};
await handleIssues([
'### Manual Fixes Needed ###',
'The configuration cannot be automatically fixed, please do the following first:',
], manualIssues);
await handleIssues([
'### Optional Manual Fixes ###',
'To fix these optional issues, please do the following manually:',
], manualIssuesOptional);
if (manualIssues.length > 0) {
this.log.info('###');
this.log.info('');
this.log.info('Bye! Run doctor again when all manual fixes have been applied!');
this.log.info('');
return true;
}
return false;
}
/**
* @param {DoctorIssue} f
*/
async runAutoFix(f) {
this.log.info(`### Fixing: ${f.error} ###`);
try {
await f.check.fix();
} catch (err) {
if (err.constructor.name === doctor.FixSkippedError.name) {
this.log.info(`### Skipped fix ###`);
return;
} else {
this.log.warn(`${err}`.replace(/\n$/g, ''));
this.log.info(`### Fix did not succeed ###`);
return;
}
}
this.log.info('Checking if this was fixed:');
const res = await f.check.diagnose();
if (res.ok) {
f.fixed = true;
this.log.info(` ${'\u2714'.green} ${res.message}`);
this.log.info(`### Fix was successfully applied ###`);
} else {
this.log.info(` ${'\u2716'.red} ${res.message}`);
this.log.info(`### Fix was applied but issue remains ###`);
}
}
async runAutoFixes() {
const autoFixes = _.filter(this.foundIssues, (f) => f.check.hasAutofix());
for (const f of autoFixes) {
await this.runAutoFix(f);
this.log.info('');
}
if (_.find(autoFixes, (f) => !f.fixed)) {
// a few issues remain.
this.log.info('Bye! A few issues remain, fix manually and/or rerun doctor!');
} else {
// nothing left to fix.
this.log.info('Bye! All issues have been fixed!');
}
this.log.info('');
}
async run() {
await this.diagnose();
if (this.reportSuccess()) {
return;
}
if (await this.reportManualIssues()) {
return;
}
await this.runAutoFixes();
}
/**
* @param {DoctorCheckResult} result
* @param {DoctorCheck} check
* @returns {DoctorIssue?}
*/
toIssue(result, check) {
if (result.ok) {
this.log.info(` ${'\u2714'.green} ${result.message}`);
return null;
}
const errorMessage = result.optional
? ` ${'\u2716'.yellow} ${result.message}`
: ` ${'\u2716'.red} ${result.message}`;
this.log.warn(errorMessage);
return {
error: errorMessage,
check,
};
}
/**
* @returns {string}
*/
buildFixMessage() {
return `${util.pluralize('required fix', this.issuesRequiredToFix.length, true)} needed, ` +
`${util.pluralize('optional fix', this.issuesOptionalToFix.length, true)} possible`;
}
/**
* @returns {boolean}
*/
reportSuccess() {
if (this.issuesRequiredToFix.length === 0 && this.issuesOptionalToFix.length === 0) {
this.log.info('Everything looks good, bye!');
this.log.info('');
return true;
}
return false;
}
}
/**
* @typedef DoctorIssue
* @property {DoctorCheck} check
* @property {string} error
* @property {boolean} [fixed]
*/
/**
* @typedef {import('@appium/types').IDoctorCheck} DoctorCheck
* @typedef {import('@appium/types').DoctorCheckResult} DoctorCheckResult
*/
+10 -4
View File
@@ -99,9 +99,12 @@ export async function getActivePlugins(pluginConfig, maxParallelImports, usePlug
`The reserved plugin name '${pluginName}' cannot be combined with other names.`
);
} else {
const suffix = _.isEmpty(pluginConfig.installedExtensions)
? `You don't have any plugins installed yet.`
: `Only the following ${_.size(pluginConfig.installedExtensions) === 1 ? `plugin is` : `plugins are`} ` +
`available: ${_.keys(pluginConfig.installedExtensions)}`;
throw new Error(
`Could not load the plugin '${pluginName}' because it is not installed. ` +
`Only the following plugins are available: ${_.keys(pluginConfig.installedExtensions)}`
`Could not load the plugin '${pluginName}' because it is not installed. ${suffix}`
);
}
}
@@ -131,9 +134,12 @@ export async function getActiveDrivers(driverConfig, maxParallelImports, useDriv
if (driverName in driverConfig.installedExtensions) {
filteredDriverNames.push(driverName);
} else {
const suffix = _.isEmpty(driverConfig.installedExtensions)
? `You don't have any drivers installed yet.`
: `Only the following ${_.size(driverConfig.installedExtensions) === 1 ? `driver is` : `drivers are`} ` +
`available: ${_.keys(driverConfig.installedExtensions)}`;
throw new Error(
`Could not load the driver '${driverName}' because it is not installed. ` +
`Only the following drivers are available: ${_.keys(driverConfig.installedExtensions)}`
`Could not load the driver '${driverName}' because it is not installed. ${suffix}`
);
}
}
+1 -2
View File
@@ -30,10 +30,9 @@ async function registerNode(data, addr, port, basePath) {
try {
data = JSON.parse(data);
} catch (err) {
logger.errorAndThrow(
throw logger.errorWithException(
`Syntax error in node configuration file ${configFilePath}: ${err.message}`
);
return;
}
}
+6
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env node
import {WebSocketServer} from 'ws';
import {init as logsinkInit} from './logsink'; // this import needs to come first since it sets up global npmlog
import logger from './logger'; // logger needs to remain second
import {
@@ -373,8 +374,13 @@ async function main(args) {
serverOpts.keepAliveTimeout = parsedArgs.keepAliveTimeout * 1000;
}
let server;
const bidiServer = new WebSocketServer({noServer: true});
bidiServer.on('connection', appiumDriver.onBidiConnection.bind(appiumDriver));
bidiServer.on('error', appiumDriver.onBidiServerError.bind(appiumDriver));
try {
server = await baseServer(serverOpts);
server.addWebSocketHandler('/bidi', bidiServer);
server.addWebSocketHandler('/bidi/:sessionId', bidiServer);
} catch (err) {
logger.error(
`Could not configure Appium server. It's possible that a driver or plugin tried ` +
+2 -1
View File
@@ -2,7 +2,7 @@ import _ from 'lodash';
import logger from './logger';
import {processCapabilities, PROTOCOLS, STANDARD_CAPS, errors} from '@appium/base-driver';
import {inspect as dump} from 'util';
import {node} from '@appium/support';
import {node, fs} from '@appium/support';
import path from 'path';
import {SERVER_SUBCOMMAND, DRIVER_TYPE, PLUGIN_TYPE} from './constants';
import os from 'node:os';
@@ -11,6 +11,7 @@ const W3C_APPIUM_PREFIX = 'appium';
const STANDARD_CAPS_LOWERCASE = new Set([...STANDARD_CAPS].map((cap) => cap.toLowerCase()));
export const V4_BROADCAST_IP = '0.0.0.0';
export const V6_BROADCAST_IP = '::';
export const npmPackage = fs.readPackageJsonFrom(__dirname);
/**
+12 -12
View File
@@ -1,6 +1,6 @@
{
"name": "appium",
"version": "2.3.0",
"version": "2.4.0",
"description": "Automation for Apps.",
"keywords": [
"automation",
@@ -60,12 +60,12 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/base-driver": "^9.4.4",
"@appium/base-plugin": "^2.2.25",
"@appium/docutils": "^1.0.1",
"@appium/schema": "^0.5.0",
"@appium/support": "^4.1.11",
"@appium/types": "^0.15.0",
"@appium/base-driver": "^9.5.0",
"@appium/base-plugin": "^2.2.26",
"@appium/docutils": "^1.0.2",
"@appium/schema": "~0.5.0",
"@appium/support": "^4.2.0",
"@appium/types": "^0.16.0",
"@sidvind/better-ajv-errors": "2.1.3",
"@types/argparse": "2.0.14",
"@types/bluebird": "3.5.42",
@@ -76,13 +76,12 @@
"ajv": "8.12.0",
"ajv-formats": "2.1.1",
"argparse": "2.0.1",
"async-lock": "1.4.0",
"async-lock": "1.4.1",
"asyncbox": "3.0.0",
"axios": "1.6.2",
"axios": "1.6.3",
"bluebird": "3.7.2",
"cross-env": "7.0.3",
"find-up": "5.0.0",
"lilconfig": "2.1.0",
"lilconfig": "3.0.0",
"lodash": "4.17.21",
"npmlog": "7.0.1",
"ora": "5.4.1",
@@ -91,9 +90,10 @@
"semver": "7.5.4",
"source-map-support": "0.5.21",
"teen_process": "2.1.1",
"type-fest": "4.8.3",
"type-fest": "4.9.0",
"winston": "3.11.0",
"wrap-ansi": "7.0.0",
"ws": "8.16.0",
"yaml": "2.3.4"
},
"engines": {
@@ -1,5 +1,5 @@
{
"devDependencies": {
"webdriverio": "8.26.2"
"webdriverio": "8.27.0"
}
}
@@ -10,6 +10,7 @@ import {
EXT_SUBCOMMAND_LIST as LIST,
EXT_SUBCOMMAND_RUN as RUN,
EXT_SUBCOMMAND_UNINSTALL as UNINSTALL,
EXT_SUBCOMMAND_DOCTOR as DOCTOR,
KNOWN_DRIVERS,
} from '../../lib/constants';
import {FAKE_DRIVER_DIR, resolveFixture} from '../helpers';
@@ -51,6 +52,11 @@ describe('Driver CLI', function () {
*/
let runUninstall;
/**
* @type {(args: string[]) => Promise<ExtRecord<DriverType>>}
*/
let runDoctor;
async function resetAppiumHome() {
await fs.rimraf(appiumHome);
await fs.mkdirp(appiumHome);
@@ -67,6 +73,8 @@ describe('Driver CLI', function () {
/** @type {ReturnType<typeof runList>} */ (await run([DRIVER_TYPE, LIST, ...args]));
runRun = async (args) =>
/** @type {ReturnType<typeof runRun>} */ (await run([DRIVER_TYPE, RUN, ...args]));
runDoctor = async (args) =>
/** @type {ReturnType<typeof runDoctor>} */ (await run([DRIVER_TYPE, DOCTOR, ...args]));
});
after(async function () {
@@ -404,6 +412,22 @@ describe('Driver CLI', function () {
});
});
});
describe('doctor', function () {
const driverName = 'fake';
before(async function () {
await resetAppiumHome();
await installLocalExtension(appiumHome, DRIVER_TYPE, FAKE_DRIVER_DIR);
});
describe('when the driver defines doctor checks', function () {
it('should load and run them', async function () {
const checks = await runDoctor([driverName]);
checks.length.should.eql(2);
});
});
});
});
/**
+174 -24
View File
@@ -1,6 +1,7 @@
// @ts-check
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';
@@ -43,6 +44,35 @@ const wdOpts = {
connectionRetryCount: 0,
};
/**
* @param {string} appiumHome
*/
async function initFakeDriver(appiumHome) {
const {driverConfig} = await loadExtensions(appiumHome);
const driverList = await runExtensionCommand(
{
driverCommand: 'list',
subcommand: DRIVER_TYPE,
suppressOutput: true,
showInstalled: true,
},
driverConfig,
);
if (!_.has(driverList, 'fake')) {
await runExtensionCommand(
{
driverCommand: 'install',
driver: FAKE_DRIVER_DIR,
installType: INSTALL_TYPE_LOCAL,
subcommand: DRIVER_TYPE,
},
driverConfig,
);
}
return await driverConfig.requireAsync('fake');
}
describe('FakeDriver via HTTP', function () {
/** @type {AppiumServer} */
let server;
@@ -64,30 +94,7 @@ describe('FakeDriver via HTTP', function () {
wdOpts.port = port = await getTestPort();
testServerBaseUrl = `http://${TEST_HOST}:${port}`;
testServerBaseSessionUrl = `${testServerBaseUrl}/session`;
// first ensure we have fakedriver installed
const {driverConfig} = await loadExtensions(appiumHome);
const driverList = await runExtensionCommand(
{
driverCommand: 'list',
subcommand: DRIVER_TYPE,
suppressOutput: true,
showInstalled: true,
},
driverConfig,
);
if (!_.has(driverList, 'fake')) {
await runExtensionCommand(
{
driverCommand: 'install',
driver: FAKE_DRIVER_DIR,
installType: INSTALL_TYPE_LOCAL,
subcommand: DRIVER_TYPE,
},
driverConfig,
);
}
FakeDriver = await driverConfig.requireAsync('fake');
FakeDriver = await initFakeDriver(appiumHome);
});
after(async function () {
@@ -536,6 +543,149 @@ describe('FakeDriver via HTTP', function () {
}
});
});
describe('Bidi protocol', function () {
withServer();
const capabilities = {...caps, webSocketUrl: true, 'appium:runClock': true};
let driver;
beforeEach(async function () {
driver = await wdio({...wdOpts, capabilities});
});
afterEach(async function () {
if (driver) {
await driver.deleteSession();
}
});
it('should respond with bidi specific capability when a driver supports it', async function () {
should.exist(driver.capabilities.webSocketUrl);
});
it('should interpret the bidi protocol and let the driver handle it by command', async function () {
should.not.exist(await driver.getUrl());
await driver.browsingContextNavigate({
context: 'foo',
url: 'https://appium.io',
wait: 'complete',
});
await driver.getUrl().should.eventually.eql('https://appium.io');
});
it('should be able to subscribe and unsubscribe to bidi events', async function () {
let collectedEvents = [];
// asyncrhonously start our listener
driver.on('clock.currentTime', ({time}) => {
collectedEvents.push(time);
});
// wait for some time to be sure clock events have happened, and assert we don't receive
// any yet
await B.delay(750);
collectedEvents.should.be.empty;
// now subscribe and wait and assert that some events have been collected
await driver.sessionSubscribe({events: ['clock.currentTime']});
await B.delay(750);
collectedEvents.should.not.be.empty;
// finally unsubscribe and wait and assert that some events have been collected
collectedEvents = [];
await driver.sessionUnsubscribe({events: ['clock.currentTime']});
await B.delay(750);
collectedEvents.should.be.empty;
});
});
});
describe('Bidi over SSL', function () {
/**
* @param {string} certPath
* @param {string} keyPath
*/
async function generateCertificate(certPath, keyPath) {
await exec('openssl', [
'req',
'-nodes',
'-new',
'-x509',
'-keyout',
keyPath,
'-out',
certPath,
'-subj',
'/C=US/ST=State/L=City/O=company/OU=Com/CN=www.testserver.local',
]);
}
/** @type {AppiumServer} */
let server;
/** @type {string} */
let appiumHome;
/** @type {import('webdriverio').Browser} */
let driver;
// since we update the FakeDriver.prototype below, make sure we update the FakeDriver which is
// actually going to be required by Appium
/** @type {import('@appium/types').DriverClass} */
let FakeDriver;
/** @type {string} */
let testServerBaseSessionUrl;
/** @type {import('sinon').SinonSandbox} */
let sandbox;
let certPath = 'certificate.cert';
let keyPath = 'certificate.key';
const capabilities = {...caps, webSocketUrl: true};
before(async function () {
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();
});
beforeEach(async function () {
driver = await wdio({...wdOpts, protocol: 'https', strictSSL: false, capabilities});
});
afterEach(async function () {
if (driver) {
await driver.deleteSession();
}
});
it('should still run bidi over ssl', async function () {
should.not.exist(await driver.getUrl());
await driver.browsingContextNavigate({
context: 'foo',
url: 'https://appium.io',
wait: 'complete',
});
await driver.getUrl().should.eventually.eql('https://appium.io');
});
});
// TODO this test only works if the log has not previously been initialized in the same process.
+15 -4
View File
@@ -1,3 +1,4 @@
import _ from 'lodash';
import {DRIVER_TYPE, PLUGIN_TYPE} from '../../lib/constants';
import {getParser} from '../../lib/cli/parser';
import {INSTALL_TYPES} from '../../lib/extension/extension-config';
@@ -349,8 +350,13 @@ describe('parser', function () {
it('should not allow an empty driver argument list', function () {
(() => p.parseArgs([DRIVER_TYPE, 'run'])).should.throw();
});
it('should not allow no driver scriptName', function () {
(() => p.parseArgs([DRIVER_TYPE, 'run', 'foo'])).should.throw();
it('should allow no driver scriptName', function () {
const args = p.parseArgs([DRIVER_TYPE, 'run', 'foo']);
args.subcommand.should.eql(DRIVER_TYPE);
args.driverCommand.should.eql('run');
args.driver.should.eql('foo');
_.isNull(args.scriptName).should.be.true;
args.json.should.eql(false);
});
it('should take a driverName and scriptName to run', function () {
const args = p.parseArgs([DRIVER_TYPE, 'run', 'foo', 'bar']);
@@ -367,8 +373,13 @@ describe('parser', function () {
it('should not allow an empty plugin argument list', function () {
(() => p.parseArgs([PLUGIN_TYPE, 'run'])).should.throw();
});
it('should not allow no plugin scriptName', function () {
(() => p.parseArgs([PLUGIN_TYPE, 'run', 'foo'])).should.throw();
it('should allow no plugin scriptName', function () {
const args = p.parseArgs([PLUGIN_TYPE, 'run', 'foo']);
args.subcommand.should.eql(PLUGIN_TYPE);
args.pluginCommand.should.eql('run');
args.plugin.should.eql('foo');
_.isNull(args.scriptName).should.be.true;
args.json.should.eql(false);
});
it('should take a pluginName and scriptName to run', function () {
const args = p.parseArgs([PLUGIN_TYPE, 'run', 'foo', 'bar']);
+1 -1
View File
@@ -19,7 +19,7 @@ export type CliCommand = CliCommandServer | CliExtensionCommand;
* Possible subcommands of {@linkcode CliCommandDriver} or
* {@linkcode CliCommandPlugin}.
*/
export type CliExtensionSubcommand = 'install' | 'list' | 'run' | 'uninstall' | 'update';
export type CliExtensionSubcommand = 'install' | 'list' | 'run' | 'uninstall' | 'update'| 'doctor';
export interface CliExtensionSubcommandListArgs {
showInstalled?: boolean;
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [9.5.0](https://github.com/appium/appium/compare/@appium/base-driver@9.4.4...@appium/base-driver@9.5.0) (2024-01-03)
### Features
* add webdriver bidi support ([2b21e66](https://github.com/appium/appium/commit/2b21e66891e8ab8c3929f04f32e94eb4efdba691))
### Bug Fixes
* **base-driver:** update dependency async-lock to v1.4.1 ([a304a1f](https://github.com/appium/appium/commit/a304a1f78e658f6f70cbe8e1efd6d06b81d8d34e))
* **support:** update dependency axios to v1.6.3 ([441b284](https://github.com/appium/appium/commit/441b2848dae28472356f37fc5d51ac27af7bbe29))
* **types:** update dependency type-fest to v4 ([#19592](https://github.com/appium/appium/issues/19592)) ([94b3580](https://github.com/appium/appium/commit/94b358022fdba3050ef94c1f881895f07e24fb75))
## [9.4.4](https://github.com/appium/appium/compare/@appium/base-driver@9.4.3...@appium/base-driver@9.4.4) (2023-12-18)
+14 -7
View File
@@ -1,16 +1,23 @@
appium-base-driver
===================
[![NPM version](http://img.shields.io/npm/v/appium-base-driver.svg)](https://npmjs.org/package/@appium/base-driver)
# @appium/base-driver
> Base class for creating other Appium drivers
[![NPM version](http://img.shields.io/npm/v/@appium/base-driver.svg)](https://npmjs.org/package/@appium/base-driver)
[![Downloads](http://img.shields.io/npm/dm/@appium/base-driver.svg)](https://npmjs.org/package/@appium/base-driver)
[![Dependency Status](https://david-dm.org/appium/@appium/base-driver.svg)](https://david-dm.org/appium/@appium/base-driver)
[![devDependency Status](https://david-dm.org/appium/@appium/base-driver/dev-status.svg)](https://david-dm.org/appium/@appium/base-driver#info=devDependencies)
[![Build Status](https://travis-ci.org/appium/@appium/base-driver.svg?branch=master)](https://travis-ci.org/appium/@appium/base-driver)
This is the parent class that all Appium drivers inherit from. This driver should not be installed
directly as it does nothing on its own. Instead, you should extend this driver when creating your
*own* Appium drivers. Check out the [Building Drivers](https://appium.io/docs/en/latest/developing/build-drivers/)
documentation for more details.
This is the parent class that all [appium](appium.io) drivers inherit from, along with a collection of globally-used Appium driver utilities. Each utility is documented in its own README in the code:
Each included utility is documented in its own README:
* [BaseDriver](lib/basedriver)
* [The Appium Express Server](lib/express)
* [The Mobile JSON Wire Protocol Encapsulation](lib/mjsonwp)
* [The JSONWP Proxy Library](lib/jsonwp-proxy)
* [The JSONWP Status Library](lib/jsonwp-status)
## License
Apache-2.0
@@ -0,0 +1,37 @@
import type {Constraints, Driver, IBidiCommands} from '@appium/types';
import type {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 IBidiCommands {}
}
const BidiCommands: IBidiCommands = {
async bidiSubscribe<C extends Constraints>(
this: BaseDriver<C>,
events: string[],
contexts: string[] = [''],
) {
for (const event of events) {
this.bidiEventSubs[event] = contexts;
}
},
async bidiUnsubscribe<C extends Constraints>(
this: BaseDriver<C>,
events: string[],
contexts: string[] = [''],
) {
for (const event of events) {
if (this.bidiEventSubs[event]) {
this.bidiEventSubs[event] = this.bidiEventSubs[event].filter((c) => !contexts.includes(c));
}
if (this.bidiEventSubs[event].length === 0) {
delete this.bidiEventSubs[event];
}
}
},
};
mixin(BidiCommands);
@@ -3,3 +3,4 @@ import './find';
import './log';
import './timeout';
import './execute';
import './bidi';
+79 -3
View File
@@ -14,15 +14,17 @@ import type {
Protocol,
RouteMatcher,
StringRecord,
BidiMethodDef,
} from '@appium/types';
import AsyncLock from 'async-lock';
import _ from 'lodash';
import {EventEmitter} from 'node:events';
import os from 'node:os';
import {DEFAULT_BASE_PATH, PROTOCOLS} from '../constants';
import {DEFAULT_BASE_PATH, PROTOCOLS, MAX_LOG_BODY_LENGTH} from '../constants';
import {errors} from '../protocol';
import DeviceSettings from './device-settings';
import helpers, {BASEDRIVER_VER} from './helpers';
import {BIDI_COMMANDS} from '../protocol/bidi-commands';
const NEW_COMMAND_TIMEOUT_MS = 60 * 1000;
@@ -99,6 +101,10 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
protocol?: Protocol;
bidiEventSubs: Record<string, string[]>;
doesSupportBidi: boolean;
constructor(opts: InitialOpts = <InitialOpts>{}, shouldValidateCaps = true) {
this._log = logger.getLogger(helpers.generateDriverLogPrefix(this as Core<C>));
@@ -132,6 +138,8 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
this.shutdownUnexpectedly = false;
this.commandsQueueGuard = new AsyncLock();
this.settings = new DeviceSettings();
this.bidiEventSubs = {};
this.doesSupportBidi = false;
}
get log() {
@@ -293,7 +301,7 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
`Potentially insecure feature '${name}' has not been ` +
`enabled. If you want to enable this feature and accept ` +
`the security ramifications, please do so by following ` +
`the documented instructions at http://appium.io/docs/en/2.0/guides/security/`
`the documented instructions at http://appium.io/docs/en/2.0/guides/security/`,
);
}
}
@@ -308,11 +316,23 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
if (!_.includes(validStrategies, strategy)) {
throw new errors.InvalidSelectorError(
`Locator Strategy '${strategy}' is not supported for this session`
`Locator Strategy '${strategy}' is not supported for this session`,
);
}
}
/**
* If this driver has requested proxying of bidi connections to an upstream bidi endpoint, this
* method should be overridden to return the URL of that websocket, to indicate that bidi
* proxying is enabled. Otherwise, a null return will indicate that bidi proxying should not be
* active and bidi commands will be handled by this driver.
*
* @returns {string | null}
*/
get bidiProxyUrl(): string | null {
return null;
}
proxyActive(sessionId: string): boolean {
return false;
}
@@ -376,4 +396,60 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
this.noCommandTimer = null;
}
}
async executeBidiCommand(bidiCmd: string, bidiParams: StringRecord): Promise<any> {
const [moduleName, methodName] = bidiCmd.split('.');
// if we don't get a valid format for bidi command name, reject
if (!moduleName || !methodName) {
throw new errors.UnknownCommandError(
`Did not receive a valid BiDi module and method name ` +
`of the form moduleName.methodName. Instead received ` +
`'${moduleName}.${methodName}'`,
);
}
// if the command module isn't part of our spec, reject
if (!BIDI_COMMANDS[moduleName]) {
throw new errors.UnknownCommandError();
}
const {command, params} = BIDI_COMMANDS[moduleName][methodName] as BidiMethodDef;
// if the command method isn't part of our spec, also reject
if (!command) {
throw new errors.UnknownCommandError();
}
// If the driver doesn't have this command, it must not be implemented
if (!this[command]) {
throw new errors.NotYetImplementedError();
}
// TODO improve param parsing and error messages along the lines of what we have in the http
// handlers
const args: any[] = [];
if (params?.required?.length) {
for (const requiredParam of params.required) {
if (_.isUndefined(bidiParams[requiredParam])) {
throw new errors.InvalidArgumentError(
`The ${requiredParam} parameter was required but you omitted it`,
);
}
args.push(bidiParams[requiredParam]);
}
}
if (params?.optional?.length) {
for (const optionalParam of params.optional) {
args.push(bidiParams[optionalParam]);
}
}
const logParams = _.truncate(JSON.stringify(bidiParams), {length: MAX_LOG_BODY_LENGTH});
this.log.debug(
`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;
}
}
+17 -20
View File
@@ -37,7 +37,7 @@ export class BaseDriver<
Settings extends StringRecord = StringRecord,
CreateResult = DefaultCreateSessionResult<C>,
DeleteResult = DefaultDeleteSessionResult,
SessionData extends StringRecord = StringRecord
SessionData extends StringRecord = StringRecord,
>
extends DriverCore<C, Settings>
implements Driver<C, CArgs, Settings, CreateResult, DeleteResult, SessionData>
@@ -55,7 +55,6 @@ export class BaseDriver<
super(opts, shouldValidateCaps);
this.caps = {} as DriverCaps<C>;
this.cliArgs = {} as CArgs & ServerArgs;
}
@@ -114,7 +113,7 @@ export class BaseDriver<
// This is needed to prevent memory leaks
this.eventEmitter.removeListener(
ON_UNEXPECTED_SHUTDOWN_EVENT,
unexpectedShutdownListener
unexpectedShutdownListener,
);
unexpectedShutdownListener = null;
}
@@ -147,7 +146,7 @@ export class BaseDriver<
}
async startUnexpectedShutdown(
err: Error = new errors.NoSuchDriverError('The driver was unexpectedly shut down!')
err: Error = new errors.NoSuchDriverError('The driver was unexpectedly shut down!'),
) {
this.eventEmitter.emit(ON_UNEXPECTED_SHUTDOWN_EVENT, err); // allow others to listen for this
this.shutdownUnexpectedly = true;
@@ -170,7 +169,7 @@ export class BaseDriver<
this.noCommandTimer = setTimeout(async () => {
this.log.warn(
`Shutting down because we waited ` +
`${this.newCommandTimeoutMs / 1000.0} seconds for a command`
`${this.newCommandTimeoutMs / 1000.0} seconds for a command`,
);
const errorMessage =
`New Command Timeout of ` +
@@ -234,23 +233,23 @@ export class BaseDriver<
w3cCapabilities2?: W3CDriverCaps<C>,
w3cCapabilities?: W3CDriverCaps<C>,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
driverData?: DriverData[]
driverData?: DriverData[],
): Promise<CreateResult> {
if (this.sessionId !== null) {
throw new errors.SessionNotCreatedError(
'Cannot create a new session while one is in progress'
'Cannot create a new session while one is in progress',
);
}
this.log.debug();
const originalCaps = _.cloneDeep(
[w3cCapabilities, w3cCapabilities1, w3cCapabilities2].find(isW3cCaps)
[w3cCapabilities, w3cCapabilities1, w3cCapabilities2].find(isW3cCaps),
);
if (!originalCaps) {
throw new errors.SessionNotCreatedError(
'Appium only supports W3C-style capability objects. ' +
'Your client is sending an older capabilities format. Please update your client library.'
'Your client is sending an older capabilities format. Please update your client library.',
);
}
@@ -258,7 +257,7 @@ export class BaseDriver<
this.originalCaps = originalCaps;
this.log.debug(
`Creating session with W3C capabilities: ${JSON.stringify(originalCaps, null, 2)}`
`Creating session with W3C capabilities: ${JSON.stringify(originalCaps, null, 2)}`,
);
let caps: DriverCaps<C>;
@@ -266,7 +265,7 @@ export class BaseDriver<
caps = processCapabilities(
originalCaps,
this._desiredCapConstraints,
this.shouldValidateCaps
this.shouldValidateCaps,
) as DriverCaps<C>;
caps = fixCaps(caps, this._desiredCapConstraints, this.log) as DriverCaps<C>;
} catch (e) {
@@ -287,7 +286,7 @@ export class BaseDriver<
throw new Error(
"The 'noReset' and 'fullReset' capabilities are mutually " +
'exclusive and should not both be set to true. You ' +
"probably meant to just use 'fullReset' on its own"
"probably meant to just use 'fullReset' on its own",
);
}
if (this.opts.noReset === true) {
@@ -354,9 +353,7 @@ export class BaseDriver<
logExtraCaps(caps: Capabilities<C>) {
const extraCaps = _.difference(_.keys(caps), _.keys(this._desiredCapConstraints));
if (extraCaps.length) {
this.log.warn(
`The following provided capabilities were not recognized by this driver:`
);
this.log.warn(`The following provided capabilities were not recognized by this driver:`);
for (const cap of extraCaps) {
this.log.warn(` ${cap}`);
}
@@ -371,11 +368,11 @@ export class BaseDriver<
try {
validateCaps(caps, this._desiredCapConstraints);
} catch (e) {
this.log.errorAndThrow(
throw this.log.errorWithException(
new errors.SessionNotCreatedError(
`The desiredCapabilities object was not valid for the ` +
`following reason(s): ${e.message}`
)
`following reason(s): ${e.message}`,
),
);
}
@@ -386,14 +383,14 @@ export class BaseDriver<
async updateSettings(newSettings: Settings) {
if (!this.settings) {
this.log.errorAndThrow('Cannot update settings; settings object not found');
throw this.log.errorWithException('Cannot update settings; settings object not found');
}
return await this.settings.update(newSettings);
}
async getSettings() {
if (!this.settings) {
this.log.errorAndThrow('Cannot get settings; settings object not found');
throw this.log.errorWithException('Cannot get settings; settings object not found');
}
return this.settings.getSettings();
}
@@ -491,7 +491,7 @@ async function unzipApp(zipPath, dstRoot, supportedAppExtensions) {
})
).sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
if (_.isEmpty(sortedBundleItems)) {
logger.errorAndThrow(
throw logger.errorWithException(
`App unzipped OK, but we could not find any '${supportedAppExtensions}' ` +
util.pluralize('bundle', supportedAppExtensions.length, false) +
` in it. Make sure your archive contains at least one package having ` +
+9 -4
View File
@@ -1,8 +1,13 @@
import B from 'bluebird';
B.config({
cancellation: true,
});
try {
B.config({
cancellation: true,
});
} catch (ign) {
// sometimes during testing this somehow gets required twice and results in an error about
// cancellation not being able to be enabled after promise has been configured
}
// BaseDriver exports
import {BaseDriver} from './basedriver/driver';
@@ -11,11 +16,11 @@ export {DeviceSettings} from './basedriver/device-settings';
export {BaseDriver};
export default BaseDriver;
export {MAX_LOG_BODY_LENGTH, DEFAULT_BASE_PATH, PROTOCOLS, W3C_ELEMENT_KEY} from './constants';
// MJSONWP exports
export * from './protocol';
export {errorFromMJSONWPStatusCode as errorFromCode} from './protocol';
export {DEFAULT_BASE_PATH, PROTOCOLS, W3C_ELEMENT_KEY} from './constants';
// Express exports
export {STATIC_DIR} from './express/static';
@@ -0,0 +1,31 @@
const SUBSCRIPTION_REQUEST_PARAMS = /** @type {const} */ ({
required: ['events'],
optional: ['contexts'],
});
const BIDI_COMMANDS = /** @type {const} */ ({
session: {
subscribe: {
command: 'bidiSubscribe',
params: SUBSCRIPTION_REQUEST_PARAMS,
},
unsubscribe: {
command: 'bidiUnsubscribe',
params: SUBSCRIPTION_REQUEST_PARAMS,
},
},
browsingContext: {
navigate: {
command: 'bidiNavigate',
params: {
required: ['context', 'url'],
optional: ['wait'],
},
},
},
});
// TODO add definitions for all bidi commands.
// spec link: https://w3c.github.io/webdriver-bidi/
export {BIDI_COMMANDS};
@@ -28,6 +28,27 @@ export class ProtocolError extends ES6Error {
set stacktrace(value) {
this._stacktrace = value;
}
/**
* 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
*/
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 = '';
}
return {
id,
type: 'error',
error: this.error,
stacktrace: this.stacktrace,
message: this.message,
};
}
}
// https://github.com/SeleniumHQ/selenium/blob/176b4a9e3082ac1926f2a436eb346760c37a5998/java/client/src/org/openqa/selenium/remote/ErrorCodes.java#L215
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/base-driver",
"version": "9.4.4",
"version": "9.5.0",
"description": "Base driver class for Appium drivers",
"keywords": [
"automation",
@@ -44,8 +44,8 @@
"test:types": "tsd"
},
"dependencies": {
"@appium/support": "^4.1.11",
"@appium/types": "^0.15.0",
"@appium/support": "^4.2.0",
"@appium/types": "^0.16.0",
"@colors/colors": "1.6.0",
"@types/async-lock": "1.4.2",
"@types/bluebird": "3.5.42",
@@ -53,9 +53,9 @@
"@types/lodash": "4.14.202",
"@types/method-override": "0.0.35",
"@types/serve-favicon": "2.5.7",
"async-lock": "1.4.0",
"async-lock": "1.4.1",
"asyncbox": "3.0.0",
"axios": "1.6.2",
"axios": "1.6.3",
"bluebird": "3.7.2",
"body-parser": "1.20.2",
"es6-error": "4.1.1",
@@ -68,7 +68,7 @@
"path-to-regexp": "6.2.1",
"serve-favicon": "2.5.0",
"source-map-support": "0.5.21",
"type-fest": "4.8.3",
"type-fest": "4.9.0",
"validate.js": "0.13.1"
},
"optionalDependencies": {
+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.2.26](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.25...@appium/base-plugin@2.2.26) (2024-01-03)
**Note:** Version bump only for package @appium/base-plugin
## [2.2.25](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.24...@appium/base-plugin@2.2.25) (2023-12-18)
+14 -2
View File
@@ -1,3 +1,15 @@
# Appium Base Plugin
# @appium/base-plugin
The base class used to create Appium plugins. This plugin should not be installed directly as it does nothing. Instead, you extend this plugin when creating your *own* Appium plugins. Check out the [Building Plugins](https://appium.io/docs/en/latest/ecosystem/build-plugins/) to learn how to make use of this base class to develop Appium plugins.
> Base class for creating other Appium plugins
[![NPM version](http://img.shields.io/npm/v/@appium/base-plugin.svg)](https://npmjs.org/package/@appium/base-plugin)
[![Downloads](http://img.shields.io/npm/dm/@appium/base-plugin.svg)](https://npmjs.org/package/@appium/base-plugin)
This is the parent class that all Appium plugins inherit from. This plugin should not be installed
directly as it does nothing on its own. Instead, you should extend this plugin when creating your
*own* Appium plugins. Check out the [Building Plugins](https://appium.io/docs/en/latest/developing/build-plugins/)
documentation for more details.
## License
Apache-2.0
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/base-plugin",
"version": "2.2.25",
"version": "2.2.26",
"description": "The base plugin used to create Appium 2.0 plugins",
"keywords": [
"automation",
@@ -40,8 +40,8 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/base-driver": "^9.4.4",
"@appium/support": "^4.1.11"
"@appium/base-driver": "^9.5.0",
"@appium/support": "^4.2.0"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
+9
View File
@@ -3,6 +3,15 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.0.31](https://github.com/appium/appium/compare/@appium/doctor@2.0.30...@appium/doctor@2.0.31) (2024-01-03)
### Bug Fixes
* **doctor:** update dependency appium-adb to v11.0.9 ([3465495](https://github.com/appium/appium/commit/34654956a3cd5a76865444ab52bd905543dfcb2c))
## [2.0.30](https://github.com/appium/appium/compare/@appium/doctor@2.0.29...@appium/doctor@2.0.30) (2023-12-18)
+8 -23
View File
@@ -1,6 +1,9 @@
appium-doctor
===================
Attempts to diagnose and fix common Node, iOS and Android configuration issues before starting Appium.
# @appium/doctor
> Attempts to diagnose and fix common Appium configuration issues
[![NPM version](http://img.shields.io/npm/v/@appium/doctor.svg)](https://npmjs.org/package/@appium/doctor)
[![Downloads](http://img.shields.io/npm/dm/@appium/doctor.svg)](https://npmjs.org/package/@appium/doctor)
### Install
@@ -26,24 +29,6 @@ Options:
-h, --help Show help [boolean]
```
## Dev
## License
## Watch
```
npm run watch
```
## Test
```
npm test
```
## Hack Cli
Use the `--demo` option to simulate the fix process.
```
appium-doctor --demo
```
Apache-2.0
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/doctor",
"version": "2.0.30",
"version": "2.0.31",
"description": "Test environment for fitness to run Appium",
"keywords": [
"automation",
@@ -44,11 +44,11 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/support": "^4.1.11",
"@appium/support": "^4.2.0",
"@colors/colors": "1.6.0",
"@types/bluebird": "3.5.42",
"@types/teen_process": "2.0.4",
"appium-adb": "11.0.8",
"appium-adb": "11.0.9",
"bluebird": "3.7.2",
"inquirer": "9.2.12",
"lodash": "4.17.21",
+24 -1
View File
@@ -3,13 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.1](https://github.com/appium/appium/compare/@appium/docutils@0.4.13...@appium/docutils@1.0.1) (2023-12-18)
## [1.0.2](https://github.com/appium/appium/compare/@appium/docutils@1.0.1...@appium/docutils@1.0.2) (2024-01-03)
### Bug Fixes
* **docutils:** update dependency lilconfig to v3 ([#19489](https://github.com/appium/appium/issues/19489)) ([ea8394a](https://github.com/appium/appium/commit/ea8394a888c835bc54f7f68174a4b6cae85339ea))
* **types:** update dependency type-fest to v4 ([#19592](https://github.com/appium/appium/issues/19592)) ([94b3580](https://github.com/appium/appium/commit/94b358022fdba3050ef94c1f881895f07e24fb75))
## [1.0.1](https://github.com/appium/appium/compare/@appium/docutils@0.4.13...@appium/docutils@1.0.1) (2023-12-18)
This includes all changes from 1.0.0.
## 1.0.0 [Unreleased]
### ⚠ BREAKING CHANGES
* `@appium/typedoc-plugin-appium` integration and all other uses of `typedoc` have been removed
* `--prefix` option has been renamed to `--deploy-prefix`
* `--rebase` flag has been removed
### Features
* **docutils:** enhance base mkdocs configuration ([#19493](https://github.com/appium/appium/issues/19493)) ([9de6f40](https://github.com/appium/appium/commit/9de6f409ec506017d6aa865cfafbd3ad5bc8d423))
* **docutils:** further adjust base mkdocs configuration ([#19527](https://github.com/appium/appium/issues/19527)) ([3dff112](https://github.com/appium/appium/commit/3dff11253db9a4681b39794b60f44dce01a7ddd3))
* **docutils:** add `--alias-type` option ([#19485](https://github.com/appium/appium/issues/19485)) ([0003304](https://github.com/appium/appium/commit/00033044e08f932b5daf1234cfb381c6f46c8bb8))
### Bug Fixes
* **docutils:** remove `@appium/typedoc-plugin-appium` and all other uses of `typedoc` ([#19465](https://github.com/appium/appium/issues/19465)) ([7528fcf](https://github.com/appium/appium/commit/7528fcf890f79f4017f5e718bb1952bf907ee479))
* **docutils:** update dependency mike to v2.0.0 ([#19485](https://github.com/appium/appium/issues/19485)) ([0003304](https://github.com/appium/appium/commit/00033044e08f932b5daf1234cfb381c6f46c8bb8))
* **docutils:** update dependency typescript to v5.2.2 ([#19472](https://github.com/appium/appium/issues/19472)) ([cb950df](https://github.com/appium/appium/commit/cb950dffb82e114341d2e0e0faa5ce6d551ec267))
+14 -10
View File
@@ -2,21 +2,25 @@
> Documentation-building utilities for Appium
## Prerequisites
[![NPM version](http://img.shields.io/npm/v/@appium/docutils.svg)](https://npmjs.org/package/@appium/docutils)
[![Downloads](http://img.shields.io/npm/dm/@appium/docutils.svg)](https://npmjs.org/package/@appium/docutils)
- Node.js in the [semantic version range](https://semver.org) `^14.17.0 || ^16.13.0 || >=18.0.0`
- Python v3+ if building HTML documentation
## Setup for Main Appium Docs
## Running for Main Appium Docs
This package does not need to be run directly. Instead, execute this command in the `packages/appium` folder of [this monorepo](https://github.com/appium/appium):
This package does not need to be run directly. Instead, execute this command in the
`packages/appium` directory:
```bash
npm run dev:docs:en
````
```
See the `scripts` field of the `package.json` for other languages.
Note that this will only build the English docs. See the `scripts` field of the `package.json` for
other languages.
## Using in Appium Extensions
## Setup for Appium Drivers and Plugins
TODO: give an example of how this package is used in an extension outside the appium monorepo, for example when it's made a part of the XCUITest repo. Or link to the part of the Appium docs where this info will live.
See the Appium docs on [Building Documentation](http://appium.io/docs/en/latest/developing/build-docs/).
## License
Apache-2.0
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/docutils",
"version": "1.0.1",
"version": "1.0.2",
"description": "Documentation generation utilities for Appium and related projects",
"keywords": [
"automation",
@@ -48,7 +48,7 @@
"start": "node ./build/lib/cli/index.js"
},
"dependencies": {
"@appium/support": "^4.1.11",
"@appium/support": "^4.2.0",
"@appium/tsconfig": "^0.x",
"@sliphua/lilconfig-ts-loader": "3.2.2",
"@types/which": "3.0.3",
@@ -57,7 +57,7 @@
"diff": "5.1.0",
"figures": "3.2.0",
"json5": "2.2.3",
"lilconfig": "2.1.0",
"lilconfig": "3.0.0",
"lodash": "4.17.21",
"log-symbols": "4.1.0",
"pkg-dir": "5.0.0",
@@ -65,7 +65,7 @@
"semver": "7.5.4",
"source-map-support": "0.5.21",
"teen_process": "2.1.1",
"type-fest": "4.8.3",
"type-fest": "4.9.0",
"typescript": "5.2.2",
"yaml": "2.3.4",
"yargs": "17.7.2",
+2 -2
View File
@@ -1,5 +1,5 @@
mkdocs==1.5.3
mkdocs-git-revision-date-localized-plugin==1.2.1
mkdocs-material==9.5.2
mkdocs-git-revision-date-localized-plugin==1.2.2
mkdocs-material==9.5.3
mkdocs-redirects==1.2.1
mike==2.0.0
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.4.20](https://github.com/appium/appium/compare/@appium/driver-test-support@0.4.19...@appium/driver-test-support@0.4.20) (2024-01-03)
### Bug Fixes
* **support:** update dependency axios to v1.6.3 ([441b284](https://github.com/appium/appium/commit/441b2848dae28472356f37fc5d51ac27af7bbe29))
* **types:** update dependency type-fest to v4 ([#19592](https://github.com/appium/appium/issues/19592)) ([94b3580](https://github.com/appium/appium/commit/94b358022fdba3050ef94c1f881895f07e24fb75))
## [0.4.19](https://github.com/appium/appium/compare/@appium/driver-test-support@0.4.18...@appium/driver-test-support@0.4.19) (2023-12-18)
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/driver-test-support",
"version": "0.4.19",
"version": "0.4.20",
"description": "Test utilities for Appium drivers",
"keywords": [
"automation",
@@ -41,10 +41,10 @@
},
"types": "./build/lib/index.d.ts",
"dependencies": {
"@appium/types": "^0.15.0",
"@appium/types": "^0.16.0",
"@types/lodash": "4.14.202",
"@types/stoppable": "1.1.3",
"axios": "1.6.2",
"axios": "1.6.3",
"bluebird": "3.7.2",
"chai": "4.3.10",
"get-port": "5.1.1",
@@ -52,7 +52,7 @@
"sinon": "17.0.1",
"source-map-support": "0.5.21",
"stoppable": "1.1.0",
"type-fest": "4.8.3"
"type-fest": "4.9.0"
},
"peerDependencies": {
"appium": "^2.0.0-beta.43",
+1 -1
View File
@@ -1,4 +1,4 @@
# @appium/eslint-config-appium
# @appium/eslint-config-appium-ts
> 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 (for TypeScript)
+3
View File
@@ -2,6 +2,9 @@
> Provides a reusable [ESLint](http://eslint.org/) [shared configuration](http://eslint.org/docs/developer-guide/shareable-configs) for [Appium](https://github.com/appium/appium) and Appium-adjacent projects.
[![NPM version](http://img.shields.io/npm/v/@appium/eslint-config-appium.svg)](https://npmjs.org/package/@appium/eslint-config-appium)
[![Downloads](http://img.shields.io/npm/dm/@appium/eslint-config-appium.svg)](https://npmjs.org/package/@appium/eslint-config-appium)
## Usage
Install the package with **`npm` v8 or newer**:
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.0.23](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@3.0.22...@appium/execute-driver-plugin@3.0.23) (2024-01-03)
### Bug Fixes
* **execute-driver-plugin:** update dependency webdriverio to v8.26.3 ([ff767b7](https://github.com/appium/appium/commit/ff767b7f25e97ec9643f691b57ca6ba159dcdcf9))
* **execute-driver-plugin:** update dependency webdriverio to v8.27.0 ([9b06b87](https://github.com/appium/appium/commit/9b06b871eb3d9c60428130442ea7ab433ce4df31))
## [3.0.22](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@3.0.21...@appium/execute-driver-plugin@3.0.22) (2023-12-18)
+42
View File
@@ -0,0 +1,42 @@
# @appium/execute-driver-plugin
> Appium plugin for running a driver script in a child process
[![NPM version](http://img.shields.io/npm/v/@appium/execute-driver-plugin.svg)](https://npmjs.org/package/@appium/execute-driver-plugin)
[![Downloads](http://img.shields.io/npm/dm/@appium/execute-driver-plugin.svg)](https://npmjs.org/package/@appium/execute-driver-plugin)
This plugin adds a new driver command that allows executing scripts in a child process. Currently,
the only supported driver type is `webdriverio`, therefore the script must also be written in JS.
## Motivation
Running a driver script in a child process adds a degree of parallelisation, which may result in
faster test execution.
## Installation
```
appium plugin install execute-driver
```
The plugin must be explicitly activated when launching the Appium server. Since the input script
can be arbitrary JavaScript, this is an insecure feature, and must also be explicitly enabled:
```
appium --use-plugins=execute-driver --allow-insecure=execute_driver_script
```
## Usage
```js
const script = `return await driver.getTimeouts();`;
const {result, logs} = await driver.executeDriverScript(script);
// 'result' contains the data returned by the script (in this case, the response to 'getTimeouts')
// 'logs' contains everything logged to console during script execution
```
Refer to your Appium client documentation for the exact syntax of this command.
## License
Apache-2.0
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/execute-driver-plugin",
"version": "3.0.22",
"version": "3.0.23",
"description": "Plugin for batching and executing driver commands with Appiums",
"keywords": [
"automation",
@@ -43,7 +43,7 @@
"lodash": "4.17.21",
"source-map-support": "0.5.21",
"vm2": "3.9.19",
"webdriverio": "8.26.2"
"webdriverio": "8.27.0"
},
"peerDependencies": {
"appium": "^2.0.0-beta.35"
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.4.0](https://github.com/appium/appium/compare/@appium/fake-driver@5.3.0...@appium/fake-driver@5.4.0) (2024-01-03)
### Features
* add webdriver bidi support ([2b21e66](https://github.com/appium/appium/commit/2b21e66891e8ab8c3929f04f32e94eb4efdba691))
* **appium,support:** Add common shortcuts for doctor checks ([#19562](https://github.com/appium/appium/issues/19562)) ([893b9e1](https://github.com/appium/appium/commit/893b9e15d64d2356ea339596e805543df51fa505))
* **appium:** Make doctor extensible ([#19542](https://github.com/appium/appium/issues/19542)) ([a30286b](https://github.com/appium/appium/commit/a30286b6e7b9753f73e55a8f9db14211b3124578))
## [5.3.0](https://github.com/appium/appium/compare/@appium/fake-driver@5.2.11...@appium/fake-driver@5.3.0) (2023-12-18)
+4 -17
View File
@@ -1,20 +1,7 @@
appium-fake-driver
===================
# @appium/fake-driver
[![Greenkeeper badge](https://badges.greenkeeper.io/appium/appium-fake-driver.svg)](https://greenkeeper.io/)
> Fake Appium driver for internal testing
Issues for this repo are disabled. Log any issues at the [main Appium repo's issue tracker](https://github.com/appium/appium/issues).
## License
Work in progress, stay tuned!
## Watch
```
npm run watch
```
## Test
```
npm test
```
Apache-2.0
+45
View File
@@ -0,0 +1,45 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const {fs, doctor} = require('@appium/support');
/** @satisfies {import('@appium/types').IDoctorCheck} */
class EnvVarAndPathCheck {
/**
* @param {string} varName
*/
constructor(varName) {
this.varName = varName;
}
async diagnose() {
const varValue = process.env[this.varName];
if (typeof varValue === 'undefined') {
return doctor.nok(`${this.varName} environment variable is NOT set!`);
}
if (await fs.exists(varValue)) {
return doctor.ok(`${this.varName} is set to: ${varValue}`);
}
return doctor.nok(`${this.varName} is set to '${varValue}' but this is NOT a valid path!`);
}
async fix() {
return (
`Make sure the environment variable ${this.varName} is properly configured for the Appium server process`
);
}
hasAutofix() {
return false;
}
isOptional() {
return false;
}
}
module.exports = {EnvVarAndPathCheck};
/**
* @typedef {import('@appium/types').DoctorCheckResult} CheckResult
*/
+6
View File
@@ -0,0 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const {EnvVarAndPathCheck} = require('./common');
const fakeCheck1 = new EnvVarAndPathCheck('FAKE1');
module.exports = {fakeCheck1};
+6
View File
@@ -0,0 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const {EnvVarAndPathCheck} = require('./common');
const fakeCheck2 = new EnvVarAndPathCheck('FAKE2');
module.exports = {fakeCheck2};
@@ -23,6 +23,9 @@ interface FakeDriverGeneralMixin {
execute(script: string, args: any[]): Promise<any>;
fakeAddition(a: number, b: number, c?: number): Promise<number>;
getLog(type: string): Promise<any>;
getUrl(): Promise<string>;
bidiNavigate(context: string, url: string): Promise<void>;
}
declare module '../driver' {
@@ -111,6 +114,14 @@ const GeneralMixin: FakeDriverGeneralMixin = {
async fakeAddition(this: FakeDriver, num1: number, num2: number, num3 = 0) {
return num1 + num2 + (num3 ?? 0);
},
async getUrl() {
return this.url;
},
async bidiNavigate(context: string, url: string) {
this.url = url;
},
};
mixin(GeneralMixin);
+42
View File
@@ -10,6 +10,9 @@ const FAKE_DRIVER_CONSTRAINTS = /** @type {const} */ ({
uniqueApp: {
isBoolean: true,
},
runClock: {
isBoolean: true,
},
});
/**
@@ -53,6 +56,12 @@ export class FakeDriver extends BaseDriver {
/** @type {Record<string,import('./fake-element').FakeElement>} */
elMap;
/** @type {string|null} */
_bidiProxyUrl;
/** @type {boolean} */
_clockRunning = false;
constructor(
opts = /** @type {import('@appium/types').InitialOpts} */ ({}),
shouldValidateCaps = true,
@@ -67,6 +76,8 @@ export class FakeDriver extends BaseDriver {
this.shook = false;
this.appModel = new FakeApp();
this.desiredCapConstraints = FAKE_DRIVER_CONSTRAINTS;
this.doesSupportBidi = true;
this._bidiProxyUrl = null;
}
proxyActive() {
@@ -77,6 +88,10 @@ export class FakeDriver extends BaseDriver {
return true;
}
get bidiProxyUrl() {
return this._bidiProxyUrl;
}
proxyReqRes(req, res) {
// fake implementation of jwp proxy req res
res.set('content-type', 'application/json');
@@ -123,9 +138,21 @@ export class FakeDriver extends BaseDriver {
);
this.caps = caps;
await this.appModel.loadApp(caps.app);
if (this.caps.runClock) {
this.startClock();
}
return [sessionId, caps];
}
/**
* @param {string} [sessionId]
* @returns {Promise<void>}
*/
async deleteSession(sessionId) {
this.stopClock();
return await super.deleteSession(sessionId);
}
get driverData() {
return {
isUnique: !!this.caps.uniqueApp,
@@ -137,6 +164,21 @@ export class FakeDriver extends BaseDriver {
return this.fakeThing;
}
async startClock() {
this._clockRunning = true;
while (this._clockRunning) {
await B.delay(500);
this.eventEmitter.emit('bidiEvent', {
method: 'clock.currentTime',
params: {time: Date.now()},
});
}
}
stopClock() {
this._clockRunning = false;
}
/**
* Set the 'thing' value (so that it can be retrieved later)
*
+7 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/fake-driver",
"version": "5.3.0",
"version": "5.4.0",
"description": "Mock driver used internally by Appium for testing. Ignore",
"keywords": [
"automation",
@@ -73,6 +73,12 @@
"fake-error": "./build/lib/scripts/fake-error.js",
"fake-success": "./build/lib/scripts/fake-success.js",
"fake-stdin": "./build/lib/scripts/fake-stdin.js"
},
"doctor": {
"checks": [
"./doctor/fake1.js",
"./doctor/fake2.js"
]
}
},
"main": "./build/lib/index.js",
+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.
## [3.1.30](https://github.com/appium/appium/compare/@appium/fake-plugin@3.1.29...@appium/fake-plugin@3.1.30) (2024-01-03)
**Note:** Version bump only for package @appium/fake-plugin
## [3.1.29](https://github.com/appium/appium/compare/@appium/fake-plugin@3.1.28...@appium/fake-plugin@3.1.29) (2023-12-18)
+6 -2
View File
@@ -1,3 +1,7 @@
# Appium Fake Plugin
# @appium/fake-plugin
This is a "fake" Appium 2.0 plugin used for testing and examples. There's no point in documenting its functionality since it's not meant to be used. If you're here at all, it's probably because you want to peek at the source code to see how a plugin is designed. So go ahead and do that!
> Fake Appium plugin for internal testing
## License
Apache-2.0
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/fake-plugin",
"version": "3.1.29",
"version": "3.1.30",
"description": "A fake Appium 2.0 plugin",
"keywords": [
"automation",
@@ -39,8 +39,8 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/base-plugin": "^2.2.25",
"@appium/support": "^4.1.11",
"@appium/base-plugin": "^2.2.26",
"@appium/support": "^4.2.0",
"@types/bluebird": "3.5.42",
"bluebird": "3.7.2",
"lodash": "4.17.21",
+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.
## [3.0.1](https://github.com/appium/appium/compare/@appium/images-plugin@3.0.0...@appium/images-plugin@3.0.1) (2024-01-03)
**Note:** Version bump only for package @appium/images-plugin
## [3.0.0](https://github.com/appium/appium/compare/@appium/images-plugin@2.1.8...@appium/images-plugin@3.0.0) (2023-12-18)
+9 -17
View File
@@ -1,35 +1,27 @@
# Appium Images Plugin
# @appium/images-plugin
This is an official Appium plugin designed to facilitate image comparison, visual testing, and image-based functional testing.
> Appium plugin for image comparison, visual testing, and image-based functional testing
[![NPM version](http://img.shields.io/npm/v/@appium/images-plugin.svg)](https://npmjs.org/package/@appium/images-plugin)
[![Downloads](http://img.shields.io/npm/dm/@appium/images-plugin.svg)](https://npmjs.org/package/@appium/images-plugin)
## Features
1. **Image Comparison** ([docs](./docs/image-comparison.md)) - A new Appium command and route that allows sending in two different images and comparing them in various ways.
2. **Finding Elements by Image** ([docs](./docs/find-by-image.md)) - Using a template image, find a matching screen region of an app and interact with it via standard Appium element semantics.
## Prerequisites
* Appium Server 2.0+
## Installation - Server
Install the plugin using Appium's plugin CLI:
## Installation
```
appium plugin install images
```
## Installation - Client
No special action is needed to make the features available in the various Appium clients, as this plugin used to be a core Appium feature and its commands are already supported in the official clients.
## Activation
The plugin will not be active unless turned on when invoking the Appium server:
The plugin must be explicitly activated when launching the Appium server:
```
appium --use-plugins=images
```
## Usage
## License
Check the docs links above for usage for each of the features provided.
Apache-2.0
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/images-plugin",
"version": "3.0.0",
"version": "3.0.1",
"description": "Plugin for working with images and image elements in Appium",
"keywords": [
"automation",
@@ -39,11 +39,11 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/opencv": "^3.0.0",
"@appium/support": "^4.1.11",
"@appium/opencv": "^3.0.1",
"@appium/support": "^4.2.0",
"lodash": "4.17.21",
"lru-cache": "10.1.0",
"sharp": "0.33.0",
"sharp": "0.33.1",
"source-map-support": "0.5.21"
},
"peerDependencies": {
+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.
## [3.0.1](https://github.com/appium/appium/compare/@appium/opencv@3.0.0...@appium/opencv@3.0.1) (2024-01-03)
**Note:** Version bump only for package @appium/opencv
## [3.0.0](https://github.com/appium/appium/compare/@appium/opencv@2.1.6...@appium/opencv@3.0.0) (2023-12-18)
+63 -2
View File
@@ -1,7 +1,68 @@
# @appium/opencv
OpenCV-related helper methods
> OpenCV-related helper methods
[![NPM version](http://img.shields.io/npm/v/@appium/opencv.svg)](https://npmjs.org/package/@appium/opencv)
[![Downloads](http://img.shields.io/npm/dm/@appium/opencv.svg)](https://npmjs.org/package/@appium/opencv)
## Installation
`npm install @appium/opencv`
This package relies on opencv.js, which is WASMed into a ~7mb dependency and run "natively" in JS. It is therefore standalone without requiring C bindings.
## Usage
### initOpenCv
Loads the opencv bindings. You only need to explicitly call this if you want to use your own opencv
methods that are not included in this module.
```js
import {initOpenCv} from '@appium/opencv';
await initOpenCv();
```
### getImagesMatches
Calculates the count of common edges between two images. The images might be rotated or resized
relatively to each other. See the function definition for more details.
```js
import {getImagesMatches} from '@appium/opencv';
import {fs} from '@appium/support';
const image1 = await fs.readFile('image1.jpg')
const image2 = await fs.readFile('image2.jpg')
const {points1, rect1, points2, rect2, totalCount, count} = await getImagesMatches(image1, image2);
```
### getImagesSimilarity
Calculates the similarity score between two images. It is expected that both images have the same
resolution. See the function definition for more details.
```js
import {getImagesSimilarity} from '@appium/opencv';
import {fs} from '@appium/support';
const image1 = await fs.readFile('image1.jpg')
const image2 = await fs.readFile('image2.jpg')
const {score} = await getImagesSimilarity(image1, image2);
```
### getImageOccurrence
Calculates the occurrence position of a partial image in the full image. See the function definition
for more details.
```js
import {getImageOccurrence} from '@appium/opencv';
import {fs} from '@appium/support';
const fullImage = await fs.readFile('image1.jpg')
const partialImage = await fs.readFile('image2.jpg')
const {rect, score} = await getImageOccurrence(fullImage, partialImage);
```
## License
Apache-2.0
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/opencv",
"version": "3.0.0",
"version": "3.0.1",
"description": "OpenCV-related helper library",
"keywords": [
"automation",
@@ -45,7 +45,7 @@
"bluebird": "3.7.2",
"lodash": "4.17.21",
"opencv-bindings": "4.5.5",
"sharp": "0.33.0",
"sharp": "0.33.1",
"source-map-support": "0.5.21"
},
"engines": {
@@ -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.
## [0.3.27](https://github.com/appium/appium/compare/@appium/plugin-test-support@0.3.26...@appium/plugin-test-support@0.3.27) (2024-01-03)
**Note:** Version bump only for package @appium/plugin-test-support
## [0.3.26](https://github.com/appium/appium/compare/@appium/plugin-test-support@0.3.25...@appium/plugin-test-support@0.3.26) (2023-12-18)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/plugin-test-support",
"version": "0.3.26",
"version": "0.3.27",
"description": "Testing utilities for Appium plugins",
"keywords": [
"automation",
@@ -41,7 +41,7 @@
"test:unit": "exit 0"
},
"dependencies": {
"@appium/types": "^0.15.0",
"@appium/types": "^0.16.0",
"get-port": "5.1.1",
"log-symbols": "4.1.0",
"source-map-support": "0.5.21",
+22 -13
View File
@@ -1,26 +1,35 @@
# Appium Relaxed Caps Plugin
# @appium/relaxed-caps-plugin
With the advent of Appium 2.0, the Appium server begins to require that all capabilities conform to the W3C [requirements for capabilities](https://www.w3.org/TR/webdriver/#capabilities). Among these requirements is one that restricts capabilities to those found in a predetermined set. Appium supports many additional capabilities as extension capabilities, and these must be accessed with the prefix `appium:` in front of the capability name.
> Appium plugin for handling extension capabilities with no prefix
There are a lot of test scripts out there that don't conform to the requirement, and so this plugin is designed to make it easy to keep running these scripts even with the new stricter capabilities requirements beginning with Appium 2.0. Basically, it inserts the `appium:` prefix for you!
[![NPM version](http://img.shields.io/npm/v/@appium/relaxed-caps-plugin.svg)](https://npmjs.org/package/@appium/relaxed-caps-plugin)
[![Downloads](http://img.shields.io/npm/dm/@appium/relaxed-caps-plugin.svg)](https://npmjs.org/package/@appium/relaxed-caps-plugin)
## Installation - Server
Appium conforms to the W3C WebDriver Protocol [requirements for capabilities](https://www.w3.org/TR/webdriver/#capabilities),
which means that all non-standard (extension) capabilities used with Appium must have a prefix
(usually this prefix is `appium:`). Any non-standard capabilities without a prefix are rejected.
Install the plugin using Appium's plugin CLI, either as a named plugin or via NPM:
This plugin can be used to automatically add the `appium:` prefix to non-standard capabilities that
do not have a prefix.
## Motivation
There are a lot of test scripts out there that don't conform to the W3C capability requirements,
so this plugin is designed to make it easy to keep running these scripts even with the stricter
capability requirements in Appium 2.
## Installation
```
appium plugin install relaxed-caps
appium plugin install --source=npm @appium/relaxed-caps-plugin
```
## Installation - Client
No special action is needed to make things work on the client side. Just keep sending in your unprefixed caps!
## Activation
The plugin will not be active unless turned on when invoking the Appium server:
The plugin must be explicitly activated when launching the Appium server:
```
appium --use-plugins=relaxed-caps
```
## License
Apache-2.0
+19
View File
@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [4.2.0](https://github.com/appium/appium/compare/@appium/support@4.1.11...@appium/support@4.2.0) (2024-01-03)
### Features
* **appium,support:** Add common shortcuts for doctor checks ([#19562](https://github.com/appium/appium/issues/19562)) ([893b9e1](https://github.com/appium/appium/commit/893b9e15d64d2356ea339596e805543df51fa505))
* Deny install/upgrade of packages which server dep does not meet the current Appium version ([#19575](https://github.com/appium/appium/issues/19575)) ([73bf68c](https://github.com/appium/appium/commit/73bf68cc602de0999e1956110be225c3fd9087c2))
### Bug Fixes
* **support:** Appium version comparison ([#19602](https://github.com/appium/appium/issues/19602)) ([eff6d33](https://github.com/appium/appium/commit/eff6d331f21070e646dc75fbc9ff7a6835cd0f13))
* **support:** update dependency axios to v1.6.3 ([441b284](https://github.com/appium/appium/commit/441b2848dae28472356f37fc5d51ac27af7bbe29))
* **support:** update dependency moment to v2.30.0 ([#19587](https://github.com/appium/appium/issues/19587)) ([e77aa41](https://github.com/appium/appium/commit/e77aa417a0e028111dd60052142a15411aca953d))
* **support:** update dependency moment to v2.30.1 ([b11366c](https://github.com/appium/appium/commit/b11366c3fcadce37567a809d172d58110f82affd))
* **types:** update dependency type-fest to v4 ([#19592](https://github.com/appium/appium/issues/19592)) ([94b3580](https://github.com/appium/appium/commit/94b358022fdba3050ef94c1f881895f07e24fb75))
## [4.1.11](https://github.com/appium/appium/compare/@appium/support@4.1.10...@appium/support@4.1.11) (2023-12-18)
+65 -102
View File
@@ -1,11 +1,15 @@
# appium-support
# @appium/support
Utility functions used to support Appium drivers and plugins.
> Utility functions used to support Appium drivers and plugins
# Usage in drivers and plugins
[![NPM version](http://img.shields.io/npm/v/@appium/support.svg)](https://npmjs.org/package/@appium/support)
[![Downloads](http://img.shields.io/npm/dm/@appium/support.svg)](https://npmjs.org/package/@appium/support)
It is recommended to have Appium as a peer dependency in driver and plugin packages.
Add the following line to `peerDependencies` section of your module's `package.json`:
## Usage in drivers and plugins
Drivers and plugins are recommended to have Appium as a peer dependency, as it already includes
these utility functions. Add the following line to `peerDependencies` section of your module's
`package.json`:
```js
"peerDependencies": {
@@ -19,7 +23,7 @@ Afterwards import it in your code similarly to the below example:
import {timing, util} from 'appium/support';
```
# Usage in helper modules
## Usage in helper modules
If you want to use this module in a helper library, which is not a driver or a plugin,
then add the following line to `dependencies` section of your module's `package.json`:
@@ -36,96 +40,49 @@ Afterwards import it in your code similarly to the below example:
import {timing, util} from '@appium/support';
```
### Categories
## Categories
All utility functions are split into a bunch of different categories. Each category has its own file under the `lib` folder. All utility functions in these files are documented.
#### fs
|Category|Description|
|-|-|
|console|Wrappers for the command line interface abstraction used by the Appium server|
|doctor|Common doctor utilities that can be used by drivers and plugins|
|env|Several helpers needed by the server to cope with internal dependencies and manifests|
|fs|Most of the functions here are just thin wrappers over utility functions available in [Promises API](https://nodejs.org/api/fs.html#promises-api)|
|image-util|Utilities to work with images. Use [sharp](https://github.com/lovell/sharp) under the hood.<br>:bangbang: Node >=18.17 is required to use these utilities|
|log-internal|Utilities needed for internal Appium log config assistance|
|logging|See [the logging section below](#logging)|
|mjpeg|Helpers needed to implement [MJPEG streaming](https://en.wikipedia.org/wiki/Motion_JPEG#Video_streaming)|
|net|Helpers needed for network interactions, for example, upload and download of files|
|node|Set of Node.js-specific utility functions needed, for example, to ensure objects immutability or to calculate their sizes|
|npm|Set of `npm`-related helpers|
|plist|Set of utilities used to read and write data from [plist](https://en.wikipedia.org/wiki/Property_List) files in javascript|
|process|Helpers for interactions with system processes. These APIs don't support Windows.|
|system|Set of helper functions needed to determine properties of the current operating system|
|tempdir|Set of helpers that allow interactions with temporary folders|
|timing|Helpers that allow to measure execution time|
|util|Miscellaneous utilities|
|zip|Helpers that allow to work with archives in `.zip ` format|
Most of the functions there are just thin wrappers over utility functions available in [Promises API](https://nodejs.org/api/fs.html#promises-api).
## logging
#### env
Several helpers needed by the server to cope with internal dependencies and manifests.
#### console
Wrappers for the command line interface abstraction used by the Appium server.
#### image-util
Utilities to work with images. Use [sharp](https://github.com/lovell/sharp) under the hood.
#### log-internal
Utilities needed for internal Appium log config assistance.
#### logging
See [below](#logger)
#### mjpeg
Helpers needed to implement [MJPEG streaming](https://en.wikipedia.org/wiki/Motion_JPEG#Video_streaming).
#### net
Helpers needed for network interactions, for example, upload and download of files.
#### node
Set of Node.js-specific utility functions needed, for example, to ensure objects immutability or to calculate their sizes.
#### npm
Set of [npm](https://en.wikipedia.org/wiki/Npm_(software))-related helpers.
#### plist
Set of utilities used to read and write data from [plist](https://en.wikipedia.org/wiki/Property_List) files in javascript.
#### process
Helpers for interactions with system processes. These APIs don't support Windows.
#### system
Set of helper functions needed to determine properties of the current operating system.
#### tempdir
Set of helpers that allow interactions with temporary folders.
#### timing
Helpers that allow to measure execution time.
#### util
Miscellaneous utilities.
#### zip
Helpers that allow to work with archives in .ZIP format.
## logger
Basic logger defaulting to [npmlog](https://github.com/npm/npmlog) with special consideration for running
tests (doesn't output logs when run with `_TESTING=1` in the env).
This is a basic logger defaulting to [npmlog](https://github.com/npm/npmlog) with special
consideration for running tests (doesn't output logs when run with `_TESTING=1`).
### Logging levels
There are a number of levels, exposed as methods on the log object, at which logging can be made. The built-in ones correspond to those of [npmlog](https://github.com/npm/npmlog#loglevelprefix-message-), and are:
`silly`, `verbose`, `info`, `http`, `warn`, and `error`. In addition there is a `debug` level.
There are a number of levels, exposed as methods on the log object, at which logging can be made.
The built-in ones correspond to those of [npmlog](https://github.com/npm/npmlog#loglevelprefix-message-),
and are: `silly`, `verbose`, `info`, `http`, `warn`, and `error`. There is also a `debug` level.
The default threshold level is `verbose`.
The logged output, by default, will be `level prefix message`. So
```js
import { logger } from 'appium-support';
let log = logger.getLogger('mymodule');
import {logging} from 'appium/support';
let log = logging.getLogger('mymodule');
log.warn('a warning');`
```
@@ -139,27 +96,25 @@ warn mymodule a warning
There are two environment variable flags that affect the way `logger` works.
`_TESTING`
- `_TESTING=1` stops output of logs when set to `1`.
`_FORCE_LOGS`
- This flag, when set to `1`, reverses the `_TESTING`
|Variable|Description|
|-|-|
|`_TESTING`|If set to `1`, logging output is stopped|
|`_FORCE_LOGS`|If set to `1`, overrides the value of `_TESTING`|
### Usage
`log.level`
- get and set the threshold level at which to display the logs. Any logs at or above this level will be displayed. The special level silent will prevent anything from being displayed ever. See [npmlog#level](https://github.com/npm/npmlog#loglevel).
- Get and set the threshold level at which to display the logs. Any logs at or above this level will
be displayed. The special level `silent` will prevent anything from being displayed ever. See
[npmlog#level](https://github.com/npm/npmlog#loglevel) for more details.
`log[level](message)`
- logs to `level`
- Logs `message` at the specified `level`
```js
import { logger } from 'appium-support';
let log = logger.getLogger('mymodule');
import {logging} from 'appium/support';
let log = logging.getLogger('mymodule');
log.info('hi!');
// => info mymodule hi!
@@ -167,11 +122,13 @@ log.info('hi!');
`log.unwrap()`
- retrieves the underlying [npmlog](https://github.com/npm/npmlog) object, in order to manage how logging is done at a low level (e.g., changing output streams, retrieving an array of messages, adding log levels, etc.).
- Retrieves the underlying [npmlog](https://github.com/npm/npmlog) object, in order to manage how
logging is done at a low level (e.g., changing output streams, retrieving an array of messages,
adding log levels, etc.).
```js
import { getLogger } from 'appium-base-driver';
let log = getLogger('mymodule');
import {logging} from 'appium/support';
let log = logging.getLogger('mymodule');
log.info('hi!');
@@ -182,18 +139,24 @@ let logs = npmlogger.record;
// logs === [ { id: 0, level: 'info', prefix: 'mymodule', message: 'hi!', messageRaw: [ 'hi!' ] }]
```
`log.errorAndThrow(error)`
`log.errorWithException(error)`
- logs the error passed in, at `error` level, and then throws the error. If the error passed in is not an instance of [Error](https://nodejs.org/api/errors.html#errors_class_error) (either directly, or a subclass of `Error`) it will be wrapped in a generic `Error` object.
- Logs the error passed in, at `error` level, and then returns the error. If the error passed in is
not an instance of [Error](https://nodejs.org/api/errors.html#errors_class_error) (either directly,
or a subclass of `Error`), it will be wrapped in a generic `Error` object.
```js
import { getLogger } from 'appium-base-driver';
let log = getLogger('mymodule');
import {logging} from 'appium/support';
let log = logging.getLogger('mymodule');
// previously there would be two lines
log.error('This is an error');
throw new Error('This is an error');
// now is compacted
log.errorAndThrow('This is an error');
throw log.errorWithException('This is an error');
```
## License
Apache-2.0
+49
View File
@@ -0,0 +1,49 @@
/**
* A shortcut for a successful required doctor check
*
* @param {string} message
* @returns {DoctorCheckResult}
*/
export function ok(message) {
return {ok: true, optional: false, message};
}
/**
* A shortcut for an unsuccessful required doctor check
*
* @param {string} message
* @returns {DoctorCheckResult}
*/
export function nok(message) {
return {ok: false, optional: false, message};
}
/**
* A shortcut for a successful optional doctor check
*
* @param {string} message
* @returns {DoctorCheckResult}
*/
export function okOptional(message) {
return {ok: true, optional: true, message};
}
/**
* A shortcut for an unsuccessful optional doctor check
*
* @param {string} message
* @returns {DoctorCheckResult}
*/
export function nokOptional(message) {
return {ok: false, optional: true, message};
}
/**
* Throw this exception in the fix() method
* of your doctor check to skip the actual fix if hasAutofix() is true
*/
export class FixSkippedError extends Error {}
/**
* @typedef {import('@appium/types').DoctorCheckResult} DoctorCheckResult
*/
+1 -1
View File
@@ -58,7 +58,7 @@ export const findAppiumDependencyPackage = _.memoize(
const readPkg = async (root) => {
try {
const pkg = await readPackageInDir(root);
const version = semver.clean(String(
const version = semver.coerce(String(
pkg?.dependencies?.appium ??
pkg?.devDependencies?.appium ??
pkg?.peerDependencies?.appium
+3
View File
@@ -14,6 +14,7 @@ import * as node from './node';
import * as timing from './timing';
import * as env from './env';
import * as console from './console';
import * as doctor from './doctor';
export {npm} from './npm';
@@ -37,6 +38,7 @@ export {
timing,
env,
console,
doctor,
};
export default {
tempDir,
@@ -56,6 +58,7 @@ export default {
timing,
env,
console,
doctor,
};
export type {ConsoleOpts} from './console';
+9 -4
View File
@@ -108,11 +108,16 @@ function getLogger(prefix = null) {
}
};
}
// add method to log an error, and throw it, for convenience
wrappedLogger.errorAndThrow = function (err) {
this.error(err);
wrappedLogger.errorWithException = function (/** @type {any[]} */ ...args) {
this.error(...args);
// make sure we have an `Error` object. Wrap if necessary
throw _.isError(err) ? err : new Error(unleakString(err));
return _.isError(args[0]) ? args[0] : new Error(args.map(unleakString).join('\n'));
};
/**
* @deprecated Use {@link errorWithException} instead
*/
wrappedLogger.errorAndThrow = function (/** @type {any[]} */ ...args) {
throw this.errorWithException(args);
};
if (!usingGlobalLog) {
// if we're not using a global log specified from some top-level package,
+1 -1
View File
@@ -73,7 +73,7 @@ async function requirePackage(packageName) {
log.debug(`Retrying load of linked package '${packageName}'`);
return require(packageName);
} catch (err) {
log.errorAndThrow(`Unable to load package '${packageName}': ${err.message}`);
throw log.errorWithException(`Unable to load package '${packageName}': ${err.message}`);
}
}
+14 -1
View File
@@ -211,7 +211,7 @@ export class NPM {
}
}
const installOpts = ['--save-dev', '--no-progress', '--no-audit', '--prefer-offline'];
const installOpts = ['--save-dev', '--no-progress', '--no-audit'];
if (!(await hasAppiumDependency(cwd))) {
if (process.env.APPIUM_OMIT_PEER_DEPS) {
installOpts.push('--omit=peer');
@@ -262,6 +262,19 @@ export class NPM {
lockFile: this._getInstallLockfilePath(cwd),
});
}
/**
* @param {string} pkg Npm package spec to query
* @param {string[]} [entries=[]] Field names to be included into the
* resulting output. By default all fields are included.
* @returns {Promise<import('@appium/types').StringRecord>}
*/
async getPackageInfo(pkg, entries = []) {
return (await this.exec('info', [pkg, ...entries], {
cwd: process.cwd(),
json: true
})).json;
}
}
export const npm = new NPM();
+4 -4
View File
@@ -31,7 +31,7 @@ async function parsePlistFile(plist, mustExist = true, quiet = true) {
// handle nonexistant file
if (!(await fs.exists(plist))) {
if (mustExist) {
log.errorAndThrow(`Plist file doesn't exist: '${plist}'`);
throw log.errorWithException(`Plist file doesn't exist: '${plist}'`);
} else {
log.debug(`Plist file '${plist}' does not exist. Returning an empty plist.`);
return {};
@@ -52,7 +52,7 @@ async function parsePlistFile(plist, mustExist = true, quiet = true) {
obj = await parseXmlPlistFile(plist);
type = 'xml';
} catch (err) {
log.errorAndThrow(`Could not parse plist file '${plist}' as XML: ${err.message}`);
throw log.errorWithException(`Could not parse plist file '${plist}' as XML: ${err.message}`);
}
}
@@ -81,14 +81,14 @@ async function updatePlistFile(
try {
obj = await parsePlistFile(plist, mustExist);
} catch (err) {
log.errorAndThrow(`Could not update plist: ${err.message}`);
throw log.errorWithException(`Could not update plist: ${err.message}`);
}
_.extend(obj, updatedFields);
let newPlist = binary ? bplistCreate(obj) : xmlplist.build(obj);
try {
await fs.writeFile(plist, newPlist);
} catch (err) {
log.errorAndThrow(`Could not save plist: ${err.message}`);
throw log.errorWithException(`Could not save plist: ${err.message}`);
}
if (!quiet) {
log.debug(`Wrote plist file '${plist}'`);
+1 -1
View File
@@ -77,7 +77,7 @@ async function open(affixes) {
// opens the file in mode 384
return {path: filePath, fd};
} catch (err) {
return log.errorAndThrow(err);
throw log.errorWithException(err);
}
}
+2 -2
View File
@@ -360,11 +360,11 @@ function quote(args) {
* caused by NodeJS string interning
* behavior described in https://bugs.chromium.org/p/v8/issues/detail?id=2869
*
* @param {*} s - The string to unleak
* @param {any} s - The string to unleak
* @return {string} Either the unleaked string or the original object converted to string
*/
function unleakString(s) {
return ` ${s}`.substr(1);
return ` ${s}`.substring(1);
}
/**
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/support",
"version": "4.1.11",
"version": "4.2.0",
"description": "Support libs used across appium packages",
"keywords": [
"automation",
@@ -42,7 +42,7 @@
},
"dependencies": {
"@appium/tsconfig": "^0.x",
"@appium/types": "^0.15.0",
"@appium/types": "^0.16.0",
"@colors/colors": "1.6.0",
"@types/archiver": "6.0.2",
"@types/base64-stream": "1.0.5",
@@ -61,7 +61,7 @@
"@types/uuid": "9.0.7",
"@types/which": "3.0.3",
"archiver": "6.0.1",
"axios": "1.6.2",
"axios": "1.6.3",
"base64-stream": "1.0.0",
"bluebird": "3.7.2",
"bplist-creator": "0.1.1",
@@ -74,7 +74,7 @@
"lockfile": "1.0.4",
"lodash": "4.17.21",
"log-symbols": "4.1.0",
"moment": "2.29.4",
"moment": "2.30.1",
"mv": "2.1.1",
"ncp": "2.0.0",
"npmlog": "7.0.1",
@@ -90,13 +90,13 @@
"source-map-support": "0.5.21",
"supports-color": "8.1.1",
"teen_process": "2.1.1",
"type-fest": "4.8.3",
"type-fest": "4.9.0",
"uuid": "9.0.1",
"which": "4.0.0",
"yauzl": "2.10.0"
},
"optionalDependencies": {
"sharp": "0.32.6"
"sharp": "0.33.1"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
@@ -28,7 +28,7 @@ describe('logger with force log', function () {
log.error('error');
assertOutputContains(writers, 'error');
(() => {
log.errorAndThrow('msg');
throw log.errorWithException('msg');
}).should.throw('msg');
assertOutputContains(writers, 'error');
assertOutputContains(writers, 'msg');
@@ -30,10 +30,10 @@ describe('normal logger', function () {
});
it('throw should not rewrite log levels outside of testing and throw error', function () {
(() => {
log.errorAndThrow('msg1');
throw log.errorWithException('msg1');
}).should.throw('msg1');
(() => {
log.errorAndThrow(new Error('msg2'));
throw log.errorWithException(new Error('msg2'));
}).should.throw('msg2');
assertOutputContains(writers, 'msg1');
assertOutputContains(writers, 'msg2');
@@ -87,7 +87,7 @@ describe('normal logger with static prefix', function () {
});
it('throw should not rewrite log levels outside of testing and throw error', function () {
(() => {
log.errorAndThrow('msg');
throw log.errorWithException('msg');
}).should.throw('msg');
assertOutputContains(writers, 'error');
assertOutputContains(writers, PREFIX);
@@ -117,7 +117,7 @@ describe('normal logger with dynamic prefix', function () {
});
it('throw should not rewrite log levels outside of testing and throw error', function () {
(() => {
log.errorAndThrow('msg');
throw log.errorWithException('msg');
}).should.throw('msg');
assertOutputContains(writers, 'error');
assertOutputContains(writers, PREFIX);
@@ -32,7 +32,7 @@ describe('test logger', function () {
log.warn(text);
log.error(text);
(() => {
log.errorAndThrow(text);
throw log.errorWithException(text);
}).should.throw(text);
assertOutputDoesntContain(writers, text);
});
+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.
## [3.0.30](https://github.com/appium/appium/compare/@appium/test-support@3.0.29...@appium/test-support@3.0.30) (2024-01-03)
**Note:** Version bump only for package @appium/test-support
## [3.0.29](https://github.com/appium/appium/compare/@appium/test-support@3.0.28...@appium/test-support@3.0.29) (2023-12-18)
+9 -17
View File
@@ -1,17 +1,17 @@
@appium/test-support
===================
# @appium/test-support
A collection of test utility lib used across Appium packages.
> A collection of test utility libs used across Appium packages
[![Build Status](https://travis-ci.org/appium/@appium/test-support.svg?branch=master)](https://travis-ci.org/appium/@appium/test-support)
[![NPM version](http://img.shields.io/npm/v/@appium/test-support.svg)](https://npmjs.org/package/@appium/test-support)
[![Downloads](http://img.shields.io/npm/dm/@appium/test-support.svg)](https://npmjs.org/package/@appium/test-support)
## Install
## Installation
```
npm install @appium/test-support --save-dev
```
## Api
## Usage
### stubEnv
@@ -59,7 +59,7 @@ describe('myTest', () => {
### withSandbox
Use when mixing up sinon apis (mocks, spies stubs).
Use when mixing up `sinon` APIs (mocks, spies, stubs).
```js
import { withSandbox } from '@appium/test-support';
@@ -145,14 +145,6 @@ can be scripted. Toward that, there are two scripts:
* `android-emu-travis-post` - waits for the device to be booted, and then goes
to its home screen
## Watch
## License
```
npm run watch
```
## Test
```
npm test
```
Apache-2.0
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/test-support",
"version": "3.0.29",
"version": "3.0.30",
"description": "A collection of test utilities used across Appium packages",
"keywords": [
"automation",
@@ -45,7 +45,7 @@
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
},
"dependencies": {
"@appium/support": "^4.1.11",
"@appium/support": "^4.2.0",
"@colors/colors": "1.6.0",
"bluebird": "3.7.2",
"lodash": "4.17.21",
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.16.0](https://github.com/appium/appium/compare/@appium/types@0.15.0...@appium/types@0.16.0) (2024-01-03)
### Features
* add webdriver bidi support ([2b21e66](https://github.com/appium/appium/commit/2b21e66891e8ab8c3929f04f32e94eb4efdba691))
* **appium,support:** Add common shortcuts for doctor checks ([#19562](https://github.com/appium/appium/issues/19562)) ([893b9e1](https://github.com/appium/appium/commit/893b9e15d64d2356ea339596e805543df51fa505))
* **appium:** Make doctor extensible ([#19542](https://github.com/appium/appium/issues/19542)) ([a30286b](https://github.com/appium/appium/commit/a30286b6e7b9753f73e55a8f9db14211b3124578))
### Bug Fixes
* **types:** update dependency type-fest to v4 ([#19592](https://github.com/appium/appium/issues/19592)) ([94b3580](https://github.com/appium/appium/commit/94b358022fdba3050ef94c1f881895f07e24fb75))
## [0.15.0](https://github.com/appium/appium/compare/@appium/types@0.14.3...@appium/types@0.15.0) (2023-12-18)
+12
View File
@@ -137,3 +137,15 @@ export type ExecuteMethodMap<T extends Plugin | Driver> = T extends Plugin
: T extends Driver
? Readonly<StringRecord<DriverExecuteMethodDef<T>>>
: never;
export interface BidiMethodDef extends BaseExecuteMethodDef {
command: string;
}
export interface BidiMethodMap {
[k: string]: BidiMethodDef;
}
export interface BidiModuleMap {
[k: string]: BidiMethodMap;
}

Some files were not shown because too many files have changed in this diff Show More