mirror of
https://github.com/appium/appium.git
synced 2026-10-31 22:32:24 +08:00
Compare commits
103
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8476bd3f65 | ||
|
|
c6aa31bda0 | ||
|
|
ffaffc11d9 | ||
|
|
8edc4143a8 | ||
|
|
ca9e79b00d | ||
|
|
d870b7aca7 | ||
|
|
6cb9e846cc | ||
|
|
5b33298ca7 | ||
|
|
e88ea8c783 | ||
|
|
c9ecc152c9 | ||
|
|
97c62676fc | ||
|
|
9e9cbdeaf1 | ||
|
|
2422e44082 | ||
|
|
05a562ca9e | ||
|
|
a17291db4e | ||
|
|
1cc2e3324a | ||
|
|
1be94b9f7a | ||
|
|
bd9ce0c78e | ||
|
|
649e997e07 | ||
|
|
d782d28326 | ||
|
|
17f25ecb33 | ||
|
|
eb559dc833 | ||
|
|
a765ff99d5 | ||
|
|
7979351543 | ||
|
|
55da22fc40 | ||
|
|
d4a0126ad5 | ||
|
|
846c60a3c7 | ||
|
|
3b054fe8d1 | ||
|
|
dafc7abaec | ||
|
|
f6eb67e838 | ||
|
|
1e07b227a1 | ||
|
|
1de5d0a0e8 | ||
|
|
25c0600b9f | ||
|
|
229f24d2e3 | ||
|
|
804b1a3c15 | ||
|
|
742ad1e9b5 | ||
|
|
71bfb5ada9 | ||
|
|
49d8b242e4 | ||
|
|
2b4b4c2a39 | ||
|
|
228e479785 | ||
|
|
a82476f2fc | ||
|
|
fcc66a1b4c | ||
|
|
d7e383c5f4 | ||
|
|
28b091a450 | ||
|
|
f55de14a4f | ||
|
|
d334ad4b4a | ||
|
|
423ef391bb | ||
|
|
283cd2c695 | ||
|
|
2e08ebbe16 | ||
|
|
7713985a50 | ||
|
|
e508e217ed | ||
|
|
f09640ccf3 | ||
|
|
8f30a5461e | ||
|
|
6ba28ebc28 | ||
|
|
c24d2ac461 | ||
|
|
4e381ada17 | ||
|
|
5b971de540 | ||
|
|
de737bba73 | ||
|
|
d8225d0176 | ||
|
|
aaab7b956e | ||
|
|
8d77bd3e06 | ||
|
|
b7dcd3f7b9 | ||
|
|
eccae91099 | ||
|
|
3425da9cf6 | ||
|
|
603d1e7fb2 | ||
|
|
f800c9d760 | ||
|
|
8f5a6c47a9 | ||
|
|
157108c70c | ||
|
|
7c5e3ee86e | ||
|
|
6e836e0860 | ||
|
|
65492d7f3b | ||
|
|
c07045ea48 | ||
|
|
1f68195ff3 | ||
|
|
fd09d7fa0f | ||
|
|
04f140a4ad | ||
|
|
3f077db410 | ||
|
|
3bda97f3c5 | ||
|
|
7c05f5c213 | ||
|
|
e68757b349 | ||
|
|
f007b46a65 | ||
|
|
f9b51ec526 | ||
|
|
884173f967 | ||
|
|
f3b1f3556c | ||
|
|
ef3d781d88 | ||
|
|
15280b80d2 | ||
|
|
365a7b1770 | ||
|
|
d1e7124d4d | ||
|
|
cb799d7ab8 | ||
|
|
ba4aa394d1 | ||
|
|
a4eda0054b | ||
|
|
dbd0d321e0 | ||
|
|
c6d5383425 | ||
|
|
859f3c430a | ||
|
|
e3560e0bee | ||
|
|
51eea37e93 | ||
|
|
053c105f6f | ||
|
|
0c50504266 | ||
|
|
1766a8d0cf | ||
|
|
dbad59f1ff | ||
|
|
b70ddd50a8 | ||
|
|
bbe04a7ac0 | ||
|
|
a7c9f6198a | ||
|
|
c5723e7655 |
@@ -53,6 +53,11 @@ labels:
|
||||
matcher:
|
||||
files: ['packages/base-plugin/**']
|
||||
|
||||
- label: '@appium/storage-plugin'
|
||||
sync: true
|
||||
matcher:
|
||||
files: ['packages/storage-plugin/**']
|
||||
|
||||
- label: '@appium/docutils'
|
||||
sync: true
|
||||
matcher:
|
||||
|
||||
@@ -56,9 +56,9 @@ jobs:
|
||||
prepare_matrix:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
versions: ${{ steps.generate-matrix.outputs.active }}
|
||||
versions: ${{ steps.generate-matrix.outputs.lts }}
|
||||
steps:
|
||||
- name: Select all active LTS versions of Node.js
|
||||
- name: Select all current LTS versions of Node.js
|
||||
id: generate-matrix
|
||||
uses: msimerson/node-lts-versions@v1
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_DOCS_PROJECT_ID }}
|
||||
CROWDIN_TOKEN: ${{ secrets.CROWDIN_DOCS_TOKEN }}
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7.0.7
|
||||
uses: peter-evans/create-pull-request@v7.0.8
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
commit-message: 'docs(appium): Update documentation translations'
|
||||
|
||||
@@ -12,9 +12,9 @@ jobs:
|
||||
prepare_matrix:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
versions: ${{ steps.generate-matrix.outputs.active }}
|
||||
versions: ${{ steps.generate-matrix.outputs.lts }}
|
||||
steps:
|
||||
- name: Select all active LTS versions of Node.js
|
||||
- name: Select all current LTS versions of Node.js
|
||||
id: generate-matrix
|
||||
uses: msimerson/node-lts-versions@v1
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ to driver/client documentation
|
||||
* [opencv](packages/opencv/CHANGELOG.md)
|
||||
* [plugin-test-support](packages/plugin-test-support/CHANGELOG.md)
|
||||
* [schema](packages/schema/CHANGELOG.md)
|
||||
* [storage-plugin](packages/storage-plugin/CHANGELOG.md)
|
||||
* [strongbox](packages/strongbox/CHANGELOG.md)
|
||||
* [support](packages/support/CHANGELOG.md)
|
||||
* [test-support](packages/test-support/CHANGELOG.md)
|
||||
|
||||
Generated
+2974
-2525
File diff suppressed because it is too large
Load Diff
+19
-19
@@ -85,17 +85,17 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@colors/colors": "1.6.0",
|
||||
"@eslint/js": "9.21.0",
|
||||
"@tsconfig/node14": "14.1.2",
|
||||
"@eslint/js": "9.25.1",
|
||||
"@tsconfig/node14": "14.1.3",
|
||||
"@types/argparse": "2.0.17",
|
||||
"@types/archiver": "6.0.3",
|
||||
"@types/async-lock": "1.4.2",
|
||||
"@types/base64-stream": "1.0.5",
|
||||
"@types/bluebird": "3.5.42",
|
||||
"@types/chai": "5.0.1",
|
||||
"@types/chai-as-promised": "8.0.1",
|
||||
"@types/diff": "7.0.1",
|
||||
"@types/express": "5.0.0",
|
||||
"@types/chai": "5.2.1",
|
||||
"@types/chai-as-promised": "8.0.2",
|
||||
"@types/diff": "7.0.2",
|
||||
"@types/express": "5.0.1",
|
||||
"@types/jsftp": "2.1.5",
|
||||
"@types/json-schema": "7.0.15",
|
||||
"@types/klaw": "3.0.6",
|
||||
@@ -105,9 +105,9 @@
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/mv": "2.1.4",
|
||||
"@types/ncp": "2.0.8",
|
||||
"@types/node": "22.13.9",
|
||||
"@types/node": "22.14.1",
|
||||
"@types/pluralize": "0.0.33",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/semver": "7.7.0",
|
||||
"@types/serve-favicon": "2.5.7",
|
||||
"@types/shell-quote": "1.7.5",
|
||||
"@types/sinon": "17.0.4",
|
||||
@@ -118,7 +118,7 @@
|
||||
"@types/uuid": "10.0.0",
|
||||
"@types/which": "3.0.4",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"@types/ws": "8.5.14",
|
||||
"@types/ws": "8.18.1",
|
||||
"@types/xmldom": "0.1.34",
|
||||
"@types/yargs": "17.0.33",
|
||||
"asyncbox": "3.0.0",
|
||||
@@ -127,16 +127,16 @@
|
||||
"conventional-changelog-conventionalcommits": "7.0.2",
|
||||
"cpy-cli": "5.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "9.21.0",
|
||||
"eslint-config-prettier": "10.0.2",
|
||||
"eslint-import-resolver-typescript": "3.8.3",
|
||||
"eslint": "9.25.1",
|
||||
"eslint-config-prettier": "10.1.2",
|
||||
"eslint-import-resolver-typescript": "4.3.4",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-mocha": "10.5.0",
|
||||
"eslint-plugin-promise": "7.2.1",
|
||||
"finalhandler": "1.3.1",
|
||||
"finalhandler": "2.1.0",
|
||||
"get-port": "5.1.1",
|
||||
"json-schema-to-typescript": "15.0.4",
|
||||
"lerna": "8.2.1",
|
||||
"lerna": "8.2.2",
|
||||
"log-symbols": "4.1.0",
|
||||
"midnight-smoker": "8.0.0",
|
||||
"mjpeg-consumer": "2.0.0",
|
||||
@@ -147,14 +147,14 @@
|
||||
"rewiremock": "3.14.5",
|
||||
"rimraf": "5.0.10",
|
||||
"serve-static": "1.16.2",
|
||||
"sinon": "19.0.2",
|
||||
"sinon": "20.0.0",
|
||||
"sync-monorepo-packages": "1.0.2",
|
||||
"ts-node": "10.9.2",
|
||||
"tsd": "0.31.2",
|
||||
"typescript": "5.7.3",
|
||||
"typescript-eslint": "8.25.0",
|
||||
"tsd": "0.32.0",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.31.0",
|
||||
"validate.js": "0.13.1",
|
||||
"webdriverio": "9.10.1",
|
||||
"webdriverio": "9.12.7",
|
||||
"ws": "8.18.1",
|
||||
"yaml-js": "0.3.1"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,32 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.18.0](https://github.com/appium/appium/compare/appium@2.17.1...appium@2.18.0) (2025-04-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **base-driver:** Accept `x-request-id` as override to generated requestId in `handleLogContext` ([#21154](https://github.com/appium/appium/issues/21154)) ([a82476f](https://github.com/appium/appium/commit/a82476f2fcb4aa3a3c4660d96fdd261a98afa4af))
|
||||
|
||||
|
||||
|
||||
## [2.17.1](https://github.com/appium/appium/compare/appium@2.17.0...appium@2.17.1) (2025-03-17)
|
||||
|
||||
**Note:** Version bump only for package appium
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.17.0](https://github.com/appium/appium/compare/appium@2.16.2...appium@2.17.0) (2025-03-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **images-plugin:** supports image elements included in actions. ([#21055](https://github.com/appium/appium/issues/21055)) ([0c50504](https://github.com/appium/appium/commit/0c50504266eb5553dafd08bfb6161f643357114f))
|
||||
|
||||
|
||||
|
||||
## [2.16.2](https://github.com/appium/appium/compare/appium@2.16.1...appium@2.16.2) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package appium
|
||||
|
||||
@@ -65,3 +65,25 @@ Find a a list of all UI elements matching a given a locator strategy and a selec
|
||||
`any`
|
||||
|
||||
A possibly-empty list of element objects
|
||||
|
||||
### `performActions`
|
||||
|
||||
`POST` **`/session/:sessionId/actions`**
|
||||
|
||||
If the actions contains image elements as origin, convert them to viewport coordinates before sending it to the external driver
|
||||
|
||||
**`See`**
|
||||
|
||||
[https://w3c.github.io/webdriver/#perform-actions](https://w3c.github.io/webdriver/#perform-actions)
|
||||
|
||||
<!-- comment source: method-signature -->
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| :------ | :------ | :------ |
|
||||
| `actions` | `ActionSequence[]` | an array of action sequences |
|
||||
|
||||
#### Response
|
||||
|
||||
`null`
|
||||
|
||||
@@ -21,4 +21,5 @@ The command listings can be found here:
|
||||
* [Execute Driver Plugin](./execute-driver-plugin.md)
|
||||
* [Images Plugin](./images-plugin.md)
|
||||
* [Relaxed Caps Plugin](./relaxed-caps-plugin.md)
|
||||
* [Storage Plugin](./storage-plugin.md)
|
||||
* [Universal XML Plugin](./universal-xml-plugin.md)
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# Plugin: storage
|
||||
|
||||
!!! tip
|
||||
|
||||
All these commands can be invoked without creating a session, allowing you to
|
||||
prepare your test environment in advance.
|
||||
|
||||
### `addStorageItem`
|
||||
|
||||
`POST` **`/storage/add`**
|
||||
|
||||
Add a new file to the storage
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| :------ | :------ | :------ |
|
||||
| `name` | `string` | the name used to save the file (must not include path separator characters) |
|
||||
| `sha1` | `string` | SHA1 hash of the file to be uploaded |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -X POST --header "Content-Type: application/json" --data '{"name":"app.ipa","sha1":"ccc963411b2621335657963322890305ebe96186"}' http://127.0.0.1:4723/storage/add
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
`AddRequestResult`
|
||||
|
||||
A JSON object in the following format:
|
||||
```json
|
||||
{
|
||||
"ws": {
|
||||
"stream": "/storage/add/ccc963411b2621335657963322890305ebe96186/stream",
|
||||
"events": "/storage/add/ccc963411b2621335657963322890305ebe96186/events"
|
||||
},
|
||||
"ttlMs": 300000
|
||||
}
|
||||
```
|
||||
|
||||
| Name | Type | Description |
|
||||
| :------ | :------ | :------ |
|
||||
| `ws.stream` | `string` | the pathname of the streaming web socket used to upload the file content |
|
||||
| `ws.events` | `string` | the pathname of the events web socket used to notify about upload success or a failure |
|
||||
| `ttlMs` | `number` | the amount of milliseconds both web sockets will be kept active before they expire, or a file payload would be successfully uploaded |
|
||||
|
||||
|
||||
### `listStorageItems`
|
||||
|
||||
`GET` **`/storage/list`**
|
||||
|
||||
List all files present in the storage
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:4723/storage/list
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
`List<StorageItem>`
|
||||
|
||||
A list of items, where each item has the following properties:
|
||||
|
||||
| Name | Type | Description |
|
||||
| :------ | :------ | :------ |
|
||||
| `name` | `string` | the name of the file in the storage |
|
||||
| `path` | `string` | full path to the file on the remote file system |
|
||||
| `size` | `number` | file size in bytes |
|
||||
|
||||
### `deleteStorageItem`
|
||||
|
||||
`POST` **`/storage/delete`**
|
||||
|
||||
Deletes a file in the storage with the specified name
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| :------ | :------ | :------ |
|
||||
| `name` | `string` | the name of the file to be deleted |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -X POST --header "Content-Type: application/json" --data '{"name":"app.ipa"}' http://127.0.0.1:4723/storage/delete
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
`boolean`
|
||||
|
||||
`false` if the file does not exist in the storage, or `true` upon successful file deletion
|
||||
|
||||
### `resetStorage`
|
||||
|
||||
`POST` **`/storage/reset`**
|
||||
|
||||
Deletes all uploaded files and stops any incomplete uploads.
|
||||
If the `APPIUM_STORAGE_KEEP_ALL` flag is enabled, all uploaded files will be preserved,
|
||||
and only the incomplete uploads will be stopped.
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:4723/storage/reset
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
`undefined`
|
||||
@@ -44,6 +44,7 @@ These drivers are not maintained by the Appium team and can be used to target ad
|
||||
|[Tizen](https://github.com/Samsung/appium-tizen-driver)|`--source=npm appium-tizen-driver`|Android|Native|Community / Samsung|
|
||||
|[TizenTV](https://github.com/headspinio/appium-tizen-tv-driver)|`--source=npm appium-tizen-tv-driver`|Samsung TV|Web|HeadSpin|
|
||||
|[Youi](https://github.com/YOU-i-Labs/appium-youiengine-driver)|`--source=npm appium-youiengine-driver`|iOS, Android, macOS, Linux, tvOS|Native|Community / You.i|
|
||||
|[NovaWindows](https://github.com/AutomateThePlanet/appium-novawindows-driver)|`--source=npm appium-novawindows-driver`|Windows|Native|Community / Automate The Planet|
|
||||
|
||||
!!! note
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ These plugins are are currently maintained by the Appium team:
|
||||
|[Execute Driver](https://github.com/appium/appium/tree/master/packages/execute-driver-plugin)|`execute-driver`|Run entire batches of commands in a single call to the Appium server|
|
||||
|[Images](https://github.com/appium/appium/tree/master/packages/images-plugin)|`images`|Image matching and comparison features|
|
||||
|[Relaxed Caps](https://github.com/appium/appium/tree/master/packages/relaxed-caps-plugin)|`relaxed-caps`|Relax Appium's requirement for vendor prefixes on capabilities|
|
||||
|[Storage](https://github.com/appium/appium/tree/master/packages/storage-plugin)|`storage`|Server-side storage with client-side management|
|
||||
|[Universal XML](https://github.com/appium/appium/tree/master/packages/universal-xml-plugin)|`universal-xml`|Instead of the standard XML format for iOS and Android, use an XML definition that is the same across both platforms|
|
||||
|
||||
### Other Plugins
|
||||
|
||||
@@ -42,7 +42,7 @@ Here is a list of all the globally-recognized Appium capabilities:
|
||||
| `appium:app` | `string` | no | The path to an installable application |
|
||||
| `appium:deviceName` | `string` | no | The name of a particular device to automate, e.g., `iPhone 14` (currently only actually useful for specifying iOS simulators, since in other situations it's typically recommended to use a specific device id via the `appium:udid` capability). |
|
||||
| `appium:platformVersion` | `string` | no | The version of a platform, e.g., for iOS, `16.0` |
|
||||
| `appium:newCommandTimeout` | `number` | no | The number of seconds the Appium server should wait for clients to send commands before deciding that the client has gone away and the session should shut down |
|
||||
| `appium:newCommandTimeout` | `number` | no | The number of seconds the Appium server should wait for clients to send commands before deciding that the client has gone away and the session should shut down. `60` seconds by default. Setting it to zero disables the timer. |
|
||||
| `appium:noReset` | `boolean` | no | If true, instruct an Appium driver to avoid its usual reset logic during session start and cleanup (default `false`) |
|
||||
| `appium:fullReset` | `boolean` | no | If true, instruct an Appium driver to augment its usual reset logic with additional steps to ensure maximum environmental reproducibility (default `false`) |
|
||||
| `appium:eventTimings` | `boolean` | no | If true, instruct an Appium driver to collect [Event Timings](./event-timing.md) (default `false`) |
|
||||
@@ -73,9 +73,9 @@ Appium supports [WebDriver BiDi](https://w3c.github.io/webdriver-bidi/) protocol
|
||||
The actual behavior depends on individual drivers while the Appium and the baseーdriver support the protocol.
|
||||
Please make sure if a driver supports the protocol and what kind of commands/events it supports in the documentation.
|
||||
|
||||
| Capability Name | Type | Description |
|
||||
|---|---|−--|
|
||||
| `webSocketUrl` | `boolean` | To enable BiDi protocol in the session. |
|
||||
| Capability Name | Type | Description |
|
||||
|-----------------|-----------|-----------------------------------------|
|
||||
| `webSocketUrl` | `boolean` | To enable BiDi protocol in the session. |
|
||||
|
||||
## Using `appium:options` to Group Capabilities
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Header Handling
|
||||
---
|
||||
|
||||
# Header Handling
|
||||
|
||||
## Request ID Tracking
|
||||
|
||||
Appium supports setting the request ID for each request through the `x-request-id` header. This is useful for tracing requests through your system, especially in environments where multiple services are involved or when debugging across different requests.
|
||||
|
||||
### Using `x-request-id`
|
||||
|
||||
When making requests to Appium, you can include an `x-request-id` header with a unique identifier. This ID will be:
|
||||
|
||||
- Used to track the request through Appium's logging system
|
||||
- Preserved across the entire request lifecycle
|
||||
- Generated automatically (as a UUID) if not provided
|
||||
@@ -6,141 +6,74 @@ This document is a guide for those who are using Appium 1 and wish to migrate to
|
||||
contains a list of breaking changes and how to migrate your environments or test suites to ensure
|
||||
compatibility with Appium 2.
|
||||
|
||||
!!! note
|
||||
Appium 2 is the biggest Appium release in over 5 years. It is _not_ focused on changing the
|
||||
automation behavior for any particular platform, but instead re-envisions Appium into an
|
||||
_ecosystem_ of automation tools:
|
||||
|
||||
Latest Appium drivers/plugins would have update since we created this documentation
|
||||
because we keep developing the ecosystem.
|
||||
|
||||
## Overview of Appium 2
|
||||
|
||||
Appium 2 is the most major new release of Appium in over 5 years. The changes in Appium 2 are _not_
|
||||
primarily related to changes in automation behaviors for specific platforms. Instead, Appium 2
|
||||
reenvisions Appium as a _platform_ where "drivers" (code projects that introduce support for
|
||||
automation of a given platform) and "plugins" (code projects that allow for overriding, altering,
|
||||
extending, or adding behaviors to Appium) can be easily created and shared.
|
||||
* The core Appium module retains only platform-agnostic functionality
|
||||
* Functionality for automating specific platforms is moved to separate _driver_ modules
|
||||
* Functionality for altering/extending Appium is moved to separate _plugin_ modules
|
||||
|
||||
At the same time, the Appium project is taking the opportunity to remove many old and deprecated
|
||||
bits of functionality.
|
||||
|
||||
Together these do introduce a few breaking changes to how Appium is installed, how drivers and
|
||||
various features are managed, and protocol support. These are detailed below.
|
||||
Since Appium 2 is a major architectural change, ^^we do not recommend directly updating your
|
||||
Appium 1 installations to Appium 2^^. Instead, please uninstall Appium 1 first, and only install
|
||||
Appium 2 afterwards.
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
### :bangbang: Default server base path
|
||||
### Drivers Installed Separately
|
||||
|
||||
With Appium 1, the server would accept commands by default on `http://localhost:4723/wd/hub`. The
|
||||
`/wd/hub` base path was a legacy convention from the days of migrating from Selenium 1 to Selenium
|
||||
2, and is no longer relevant. As such the default base path for the server is now `/`. If you want
|
||||
to retain the old behaviour, you can set the base path via a command line argument as follows:
|
||||
When installing Appium 1, all available drivers would be installed alongside the main Appium
|
||||
server. In Appium 2, due to its modular structure, this is no longer the case - by default,
|
||||
installing it only installs the core Appium server, without any drivers.
|
||||
|
||||
```
|
||||
appium --base-path=/wd/hub
|
||||
```
|
||||
|
||||
You can also set server arguments as [Config file](./config.md) properties.
|
||||
|
||||
### :bangbang: Installing drivers during setup
|
||||
|
||||
When you installed Appium 1, all available drivers would be installed at the same time as the main
|
||||
Appium server. This is no longer the case. Simply installing Appium 2 (e.g., by `npm i -g appium`),
|
||||
will install the Appium server only, but no drivers. To install drivers, you must instead use the
|
||||
new [Appium extension CLI](../cli/extensions.md). For example, to install the latest versions of the
|
||||
XCUITest and UiAutomator2 drivers, after installing Appium you would run the following commands:
|
||||
|
||||
```bash
|
||||
appium driver install uiautomator2 # installs the latest driver version
|
||||
appium driver install xcuitest@4.12.2 # installs a specific driver version
|
||||
```
|
||||
|
||||
At this point, your drivers are installed and ready. There's a lot more you can do with the Appium 2
|
||||
command line, so be sure to check out [its documentation](../cli/index.md). If you're running in a CI
|
||||
environment or want to install Appium along with some drivers all in one step, you can do so using
|
||||
some special flags during install, for example:
|
||||
When it comes to installing Appium 2 drivers, there are several approaches you can take:
|
||||
|
||||
* Add the `--drivers` flag when installing Appium, for example:
|
||||
```bash
|
||||
npm i -g appium --drivers=xcuitest,uiautomator2
|
||||
```
|
||||
* Use the [Appium Extension CLI](../cli/extensions.md), for example:
|
||||
```bash
|
||||
appium driver install uiautomator2
|
||||
```
|
||||
* Use the [Appium Setup CLI command](../cli/setup.md) (added in Appium `2.6`), for example:
|
||||
```bash
|
||||
appium setup mobile
|
||||
```
|
||||
|
||||
This will install Appium and the two drivers for you in one go. Please uninstall any existing Appium 1
|
||||
`npm` packages (with `npm uninstall -g appium`) if you get an installation or startup error.
|
||||
Check the [Managing Drivers and Plugins guide](./managing-exts.md) for more information.
|
||||
|
||||
### :bangbang: Drivers installation path
|
||||
!!! info "Actions Needed"
|
||||
|
||||
When you installed Appium 1, all available drivers would be installed at the same time as the main
|
||||
Appium server, at `/path/to/appium/node_modules`. For example, `appium-webdriveragent` was located
|
||||
When installing Appium 2, use one of the above approaches for installing your desired drivers
|
||||
|
||||
### Driver Installation Path Changed
|
||||
|
||||
When installing Appium 1, all available drivers would be installed as dependencies of the main
|
||||
Appium server, in `/path/to/appium/node_modules`. For example, `appium-webdriveragent` was located
|
||||
at `/path/to/appium/node_modules/appium-xcuitest-driver/node_modules/appium-webdriveragent`.
|
||||
|
||||
Appium 2 installs such dependencies in the path defined by the `APPIUM_HOME` environment variable.
|
||||
The default path is `~/.appium`. So, `appium-webdriveragent` would now be located at
|
||||
`$APPIUM_HOME/node_modules/appium-xcuitest-driver/node_modules/appium-webdriveragent`.
|
||||
In Appium 2, drivers (and plugins) are installed at the path defined by the `APPIUM_HOME`
|
||||
environment variable, whose default value is `~/.appium`. So, `appium-webdriveragent` would now be
|
||||
located at `$APPIUM_HOME/node_modules/appium-xcuitest-driver/node_modules/appium-webdriveragent`.
|
||||
|
||||
### :bangbang: Chromedriver installation flags
|
||||
!!! info "Actions Needed"
|
||||
|
||||
In Appium 1, it was possible to customize the way Chromedriver was installed (as part of the
|
||||
UiAutomator2 driver for example), using the following command line flags:
|
||||
If your code uses paths to Appium driver files, update it to use the `APPIUM_HOME` environment
|
||||
variable
|
||||
|
||||
* `--chromedriver-skip-install`
|
||||
* `--chromedriver-version`
|
||||
* `--chromedriver-cdnurl`
|
||||
### Drivers Updated Separately
|
||||
|
||||
Because Appium 2 now installs drivers for you, and because these flags were implemented as `npm`
|
||||
config flags, they will no longer work. Instead, use the following environment variables during
|
||||
driver installation:
|
||||
In Appium 1, in order to get updates to your drivers, you would simply wait for those updates to be
|
||||
rolled into a new release of Appium, and then update your Appium version. With Appium 2, since the
|
||||
server and drivers are separate packages, they can release new versions independently from each
|
||||
other - this means that you no longer need to wait for a new Appium server release, but can install
|
||||
the latest driver versions right away.
|
||||
|
||||
* `APPIUM_SKIP_CHROMEDRIVER_INSTALL`
|
||||
* `CHROMEDRIVER_VERSION`
|
||||
* `CHROMEDRIVER_CDNURL`
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
APPIUM_SKIP_CHROMEDRIVER_INSTALL=1 appium driver install uiautomator2
|
||||
```
|
||||
|
||||
### :bangbang: Driver-specific command line options
|
||||
|
||||
With Appium 1, command-line options specific to particular drivers were all hosted on the main
|
||||
Appium server. So, for example, `--chromedriver-executable` was a CLI parameter you could use with
|
||||
Appium to set the location of a specific Chromedriver version for use with, say, the UiAutomator2 driver.
|
||||
|
||||
With Appium 2, all driver- and platform-specific CLI params have been moved to the drivers themselves.
|
||||
To access the corresponding functionality, you'll need to refer to the driver/plugin documentation.
|
||||
In some cases, the extension will continue to expose CLI parameters. For example, the XCUITest driver
|
||||
used to expose a parameter `--webdriveragent-port`. Now, to access this parameter, it should be
|
||||
prefixed with `driver-xcuitest`, to differentiate it from parameters other drivers might also expose.
|
||||
To use this parameter, you thus need to start Appium with something like:
|
||||
|
||||
```bash
|
||||
appium --driver-xcuitest-webdriveragent-port=5000
|
||||
```
|
||||
|
||||
Some drivers have done away with CLI args entirely in favour of default capabilities. With the
|
||||
above-mentioned `--chromedriver-executable` parameter for example, you now need to take advantage
|
||||
of the `appium:chromedriverExecutable` capability supported by the UiAutomator2 driver. To set this
|
||||
capability from the command line, do the following:
|
||||
|
||||
```bash
|
||||
appium --default-capabilities '{"appium:chromedriverExecutable": "/path/to/chromedriver"}'
|
||||
```
|
||||
|
||||
### :bangbang: Driver-specific automation commands
|
||||
|
||||
The definition of certain commands that pertain only to specific drivers has been moved to those
|
||||
drivers' implementations. For example, `pressKeyCode` is specific to the UiAutomator2 driver and is
|
||||
now understood only by that driver. In practice, the only breaking change here is the kind of error
|
||||
you would encounter if the appropriate driver is not installed. Previously, you would get a `501
|
||||
Not Yet Implemented` error if using a driver that didn't implement the command. Now, you will get
|
||||
a `404 Not Found` error because if a driver that doesn't know about the command is not active, the
|
||||
main Appium server will not define the route corresponding to the command.
|
||||
|
||||
### :bangbang: Driver updates
|
||||
|
||||
In the past, to get updates to your iOS or Android drivers, you'd simply wait for those updates to
|
||||
be rolled into a new release of Appium, and then update your Appium version. With Appium 2, the
|
||||
Appium server and the Appium drivers are versioned and released separately. This means that drivers
|
||||
can be on their own release cadence, and you can get the latest driver version right away, rather
|
||||
than waiting for a new Appium server release. The way to check for driver updates is with the CLI:
|
||||
Checking for driver updates is done by using the [Appium Extension CLI](../cli/extensions.md):
|
||||
|
||||
```bash
|
||||
appium driver list --updates
|
||||
@@ -152,213 +85,284 @@ If any updates are available, you can then run the `update` command for any give
|
||||
appium driver update xcuitest
|
||||
```
|
||||
|
||||
(For a complete description of the update command, check out the
|
||||
[Extension CLI](../cli/extensions.md#update) documentation)
|
||||
|
||||
To update the Appium server itself, you do the same thing as in the past: `npm install -g appium`.
|
||||
Now, installing new versions of the Appium server will leave your drivers intact, so the whole
|
||||
process will be much more quick.
|
||||
|
||||
If you would like to update to a specific driver version, not the latest, please uninstall the driver
|
||||
and install the desired version using the `install` subcommand instead of `update`.
|
||||
Updating the Appium server itself is the same as before:
|
||||
|
||||
```bash
|
||||
appium driver uninstall xcuitest
|
||||
appium driver install xcuitest@4.11.1
|
||||
npm update -g appium
|
||||
```
|
||||
|
||||
### :bangbang: Protocol changes
|
||||
However, in Appium 2 this process is a lot quicker, since drivers are no longer bundled with the
|
||||
server package.
|
||||
|
||||
!!! info "Actions Needed"
|
||||
|
||||
Make sure to use the [Appium Extension CLI](../cli/extensions.md) to manage your drivers
|
||||
|
||||
### Deprecated Packages No Longer Supported
|
||||
|
||||
The Appium 1 ecosystem included several drivers, clients and other packages that had since been
|
||||
deprecated and replaced with newer packages. Appium 2 no longer includes support for these packages,
|
||||
and it is recommended to migrate to the following replacements:
|
||||
|
||||
|Appium 1 Package|Replacement in Appium 2|
|
||||
|--|--|
|
||||
|iOS Driver|[XCUITest Driver](https://appium.github.io/appium-xcuitest-driver/latest/)|
|
||||
|UiAutomator Driver|[UiAutomator2](https://github.com/appium/appium-uiautomator2-driver/)|
|
||||
|`wd` Client|[WebdriverIO Client](https://webdriver.io/)|
|
||||
|Appium Desktop|[Appium Inspector](https://github.com/appium/appium-inspector)|
|
||||
|
||||
!!! info "Actions Needed"
|
||||
|
||||
If you are using any of the aforementioned package(s), migrate to their recommended replacement(s)
|
||||
|
||||
### Default Server Base Path Changed
|
||||
|
||||
In Appium 1, the default Appium server URL was `http://localhost:4723/wd/hub`, where the `/wd/hub`
|
||||
part (the base path) was a legacy convention from Selenium 1. Appium 2 changes the default base
|
||||
path to `/`, therefore the default server URL is now `http://localhost:4723/`.
|
||||
|
||||
!!! info "Actions Needed"
|
||||
|
||||
In your test scripts, change the base path of the target server URL from `/wd/hub` to `/`.
|
||||
Alternatively, you can retain the Appium 1 base path by launching Appium with the
|
||||
`--base-path=/wd/hub` [command-line argument](../cli/args.md).
|
||||
|
||||
### Server Port 0 No Longer Supported
|
||||
|
||||
In Appium 1, it was possible to specify `--port 0` during server startup, which had the effect of
|
||||
starting Appium on a random free port. Appium 2 no longer allows this, and requires port values to
|
||||
be `1` or higher. If you wish to start Appium on a random port, you must now take care of this on
|
||||
your own prior to launching the server.
|
||||
|
||||
!!! info "Actions Needed"
|
||||
|
||||
If you are launching Appium with `--port 0`, change the port number value to `1` or higher
|
||||
|
||||
### Driver-Specific CLI Options Changed
|
||||
|
||||
With Appium 1, command-line options specific to particular drivers were all hosted on the main
|
||||
Appium server. So, for example, `--chromedriver-executable` was a CLI parameter you could use to
|
||||
set the Chromedriver location for the UiAutomator2 driver.
|
||||
|
||||
In Appium 2, all driver-specific CLI options have been moved to the drivers themselves. However,
|
||||
depending on the driver, these options may now need to be passed in another way:
|
||||
|
||||
* Some options can still be passed as different CLI flags, for example:
|
||||
```bash
|
||||
appium --webdriveragent-port=5000 # Appium 1
|
||||
appium --driver-xcuitest-webdriveragent-port=5000 # Appium 2
|
||||
```
|
||||
* Some options can now be passed as environment variables, for example:
|
||||
```bash
|
||||
appium --chromedriver-version=100 # Appium 1
|
||||
CHROMEDRIVER_VERSION=100 appium # Appium 2
|
||||
```
|
||||
* Some options can now be passed as [capabilities](https://appium.io/docs/en/latest/guides/caps/),
|
||||
for example:
|
||||
```
|
||||
appium --chromedriver-executable=/path/to/chromedriver # Appium 1
|
||||
{"appium:chromedriverExecutable": "/path/to/chromedriver"} # Appium 2
|
||||
```
|
||||
|
||||
!!! info "Actions Needed"
|
||||
|
||||
If you are using driver-specific CLI options, refer to that driver's documentation for how to
|
||||
apply them in Appium 2
|
||||
|
||||
### Filepaths No Longer Supported for Some CLI Options
|
||||
|
||||
In Appium 1, some server command-line options could be invoked by passing a filepath as their
|
||||
value, and Appium would then parse the contents of that file as the actual value for that option.
|
||||
There were four options that supported this:
|
||||
|
||||
* `--nodeconfig`
|
||||
* `--default-capabilities`
|
||||
* `--allow-insecure`
|
||||
* `--deny-insecure`
|
||||
|
||||
Appium 2 no longer attempts to parse the contents of filepaths passed to these options, and offers
|
||||
two ways to specify the value for these options:
|
||||
|
||||
* As strings, directly on the command line
|
||||
* `--nodeconfig` / `--default-capabilities`: JSON string
|
||||
* `--allow-insecure` / `--deny-insecure`: comma-separated list
|
||||
* In the [Appium Configuration file](./config.md)
|
||||
|
||||
!!! info "Actions Needed"
|
||||
|
||||
If you are using any of the aforementioned CLI options with a filepath value, update your code
|
||||
to pass the file contents either directly or through the Appium config file
|
||||
|
||||
### Old Protocols Dropped
|
||||
|
||||
Appium's API is based on the [W3C WebDriver Protocol](https://www.w3.org/TR/webdriver/), and it has
|
||||
supported this protocol for years. Before the W3C WebDriver Protocol was designed as a web standard,
|
||||
several other protocols were used for both Selenium and Appium. These protocols were the "JSONWP"
|
||||
(JSON Wire Protocol) and "MJSONWP" (Mobile JSON Wire Protocol). The W3C Protocol differs from the
|
||||
(M)JSONWP protocols in a few small ways.
|
||||
supported this protocol for years. Before this protocol was designed as a web standard, the
|
||||
[JSON Wire Protocol](https://www.selenium.dev/documentation/legacy/json_wire_protocol/) (JSONWP)
|
||||
and [Mobile JSON Wire Protocol](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md)
|
||||
(MJSONWP) were used instead.
|
||||
|
||||
Up until Appium 2, Appium supported both protocols, so that older Selenium/Appium clients could
|
||||
still communicate with newer Appium servers. Appium 2 removes support for older protocols and is
|
||||
now only compatible with the W3C WebDriver Protocol.
|
||||
In Appium 1, all of these protocols were supported, so that older Selenium/Appium clients could
|
||||
still communicate with newer Appium servers. Appium 2 removes support for JSONWP/MJSONWP and is now
|
||||
only compatible with the W3C WebDriver Protocol.
|
||||
|
||||
### :bangbang: Capabilities must use vendor prefix
|
||||
!!! info "Actions Needed"
|
||||
|
||||
One significant difference between old and new protocols is in the format of capabilities.
|
||||
Previously called "desired capabilities", and now called simply "capabilities", there is now a
|
||||
requirement for a so-called "vendor prefix" on any non-standard capabilities. The list of standard
|
||||
capabilities is given in the [WebDriver Protocol spec](https://www.w3.org/TR/webdriver/#capabilities),
|
||||
and includes a few commonly used capabilities such as `browserName` and `platformName`.
|
||||
Make sure you are using Selenium/Appium clients compatible with the W3C WebDriver Protocol
|
||||
|
||||
These standard capabilities continue to be used as-is. All other capabilities must include a
|
||||
"vendor prefix" in their name. A vendor prefix is a string followed by a colon, such as `appium:`.
|
||||
Most of Appium's capabilities go beyond the standard W3C capabilities and must therefore include
|
||||
vendor prefixes (we recommend that you use `appium:` unless directed otherwise by documentation).
|
||||
For example:
|
||||
### Capabilities Require Vendor Prefix
|
||||
|
||||
- `appium:app`
|
||||
- `appium:noReset`
|
||||
- `appium:deviceName`
|
||||
In Appium 1, in order to create a session, you had to specify certain desired capabilities, which
|
||||
would indicate session parameters, e.g. the driver you want to use. Appium 2 retains this behavior
|
||||
and continues to accept desired capabilities (now renamed simply to 'capabilities'), but as part of
|
||||
the W3C WebDriver Protocol specification, all non-standard capabilities are now required to use a
|
||||
vendor prefix.
|
||||
|
||||
This requirement may or may not be a breaking change for your test suites when targeting Appium 2.
|
||||
If you're using an updated Appium client (at least one maintained by the Appium team), the client
|
||||
will add the `appium:` prefix for you on all necessary capabilities automatically. New versions of
|
||||
[Appium Inspector](https://github.com/appium/appium-inspector) will also do this. Cloud-based Appium
|
||||
providers may also do this. So simply be aware that if you get any messages to the effect that your
|
||||
capabilities lack a vendor prefix, this is how you solve that problem.
|
||||
The list of standard capabilities is described in the [WebDriver Protocol specification](https://www.w3.org/TR/webdriver/#capabilities),
|
||||
and includes a few commonly used capabilities like `browserName` and `platformName`. All other
|
||||
capabilities must now start with the vendor name and a colon (the vendor prefix), for example,
|
||||
`moz:` or `goog:`. Since most of Appium's capabilities go beyond the standard W3C capabilities,
|
||||
all of them must include the `appium:` prefix (unless specified otherwise):
|
||||
|
||||
To make everyone's lives a bit easier, we've also introduced the option of grouping up all
|
||||
Appium-related capabilities into one object capability, `appium:options`. You can bundle together
|
||||
anything that you would normally put an `appium:` prefix on into this one capability. Here's an
|
||||
example (in raw JSON) of how you might start an iOS session on the Safari browser using `appium:options`:
|
||||
|
||||
```json
|
||||
{
|
||||
"platformName": "iOS",
|
||||
"browserName": "Safari",
|
||||
"appium:options": {
|
||||
"platformVersion": "14.4",
|
||||
"deviceName": "iPhone 11",
|
||||
"automationName": "XCUITest"
|
||||
}
|
||||
}
|
||||
```
|
||||
deviceName # Appium 1
|
||||
appium:deviceName # Appium 2
|
||||
```
|
||||
|
||||
(Of course, each client will have a different way of creating structured capabilities like
|
||||
`appium:options` or other ones that you might have seen such as `goog:chromeOptions`).
|
||||
This requirement may or may not be a breaking change for your test suites. Up-to-date versions of
|
||||
official Appium clients and the Appium Inspector will automatically add the `appium:` prefix to all
|
||||
non-standard capabilities, and the same may apply to cloud-based Appium providers.
|
||||
|
||||
!!! note
|
||||
Additionally, if you are starting a session with multiple Appium-specific capabilities (which will
|
||||
likely be the case), it may seem repetitive to add the `appium:` prefix to each individual
|
||||
capability. To avoid this, you can optionally group up all these capabilities under a single object
|
||||
capability, `appium:options`, for example:
|
||||
|
||||
=== "Default Approach"
|
||||
|
||||
```json
|
||||
{
|
||||
"platformName": "iOS",
|
||||
"browserName": "Safari",
|
||||
"appium:platformVersion": "14.4",
|
||||
"appium:deviceName": "iPhone 11",
|
||||
"appium:automationName": "XCUITest"
|
||||
}
|
||||
```
|
||||
|
||||
=== "With `appium:options`"
|
||||
|
||||
```json
|
||||
{
|
||||
"platformName": "iOS",
|
||||
"browserName": "Safari",
|
||||
"appium:options": {
|
||||
"platformVersion": "14.4",
|
||||
"deviceName": "iPhone 11",
|
||||
"automationName": "XCUITest"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
Capabilities included in the `appium:options` object will overwrite capabilities of the same
|
||||
name that are used outside of this object. (The `appium:options` syntax support by cloud
|
||||
providers may vary.)
|
||||
name that are used outside of this object. Note that cloud provider support for the
|
||||
`appium:options` syntax may vary.
|
||||
|
||||
For more information on capabilities, have a look at the [Capabilities Guide](./caps.md).
|
||||
|
||||
### :bangbang: Removed Commands
|
||||
!!! info "Actions Needed"
|
||||
|
||||
In addition to commands which have been moved to driver implementations, commands which were a part
|
||||
of the old JSON Wire Protocol and not a part of the W3C Protocol are no longer available:
|
||||
Add the `appium:` prefix to all Appium-specific capabilities used in your tests, or wrap them
|
||||
inside an `appium:options` object
|
||||
|
||||
- TODO (these commands are being identified and removed and will be updated here when complete)
|
||||
### Advanced Features Moved to Plugins
|
||||
|
||||
If you use a modern Appium or Selenium client, you should no longer have access to these anyway,
|
||||
so any breaking changes should appear on the client side first and foremost.
|
||||
One of the design goals for Appium 2 is to extract non-core features into special extensions called
|
||||
[plugins](../ecosystem/plugins.md). Two such features of Appium 1 have been moved to plugins, and
|
||||
are no longer bundled with Appium 2:
|
||||
|
||||
### :bangbang: Image analysis features moved to plugin
|
||||
|Feature|Plugin Name|
|
||||
|--|--|
|
||||
|Image-related features (comparison, search by image, etc...)|[`images`](https://github.com/appium/appium/tree/master/packages/images-plugin)|
|
||||
|The Execute Driver Script feature|[`execute-driver`](https://github.com/appium/appium/tree/master/packages/execute-driver-plugin)|
|
||||
|
||||
One of the design goals for Appium 2 is to migrate non-core features into special extensions called
|
||||
[plugins](../ecosystem/plugins.md). This allows people to opt into features which require extra time
|
||||
to download or extra system setup. The various image-related features of Appium (image comparison,
|
||||
finding elements by image, etc...) have been moved into an officially supported plugin called
|
||||
[images](https://github.com/appium/appium/tree/master/packages/images-plugin).
|
||||
!!! info "Actions Needed"
|
||||
|
||||
If you use these image-related methods, to continue accessing them you will need to do two things:
|
||||
If you were using the image-related features and/or the execute driver script feature in Appium
|
||||
1, first install their plugin(s):
|
||||
```
|
||||
appium plugin install images
|
||||
appium plugin install execute-driver
|
||||
```
|
||||
Afterwards, make sure to activate the plugin(s) upon launching the Appium server:
|
||||
```
|
||||
appium --use-plugins=images,execute-driver
|
||||
```
|
||||
|
||||
1. Install the plugin: `appium plugin install images`
|
||||
2. Ensure you start the Appium server with access to run the plugin by including it in the list of
|
||||
plugins designated on the command line, e.g., `appium --use-plugins=images`
|
||||
### Endpoint Changes
|
||||
|
||||
Image-related commands will also be removed on the client side of things, which means you will need
|
||||
to follow the instructions on the plugin README for installing client-side plugins to access these features.
|
||||
A few server endpoints used in Appium 1 were accepting old or unused parameters. Appium 2 removes
|
||||
support for these parameters. The following is a list of these changed endpoints, along with the
|
||||
parameters they no longer accept, as well as the parameters they continue to accept in Appium 2.
|
||||
|
||||
### :bangbang: Execute Driver Script command moved to plugin
|
||||
* `POST /session/:sessionId/appium/device/gsm_signal`
|
||||
* :octicons-x-24: `signalStrengh`
|
||||
* :octicons-check-24: `signalStrength`
|
||||
* `POST /session/:sessionId/appium/element/:elementId/value`
|
||||
* :octicons-x-24: `value`
|
||||
* :octicons-check-24: `text`
|
||||
* `POST /session/:sessionId/appium/element/:elementId/replace_value`
|
||||
* :octicons-x-24: `value`
|
||||
* :octicons-check-24: `text`
|
||||
|
||||
If you use the advanced Execute Driver Script feature (which allows you to send in a WebdriverIO
|
||||
script to have it executed completely on the server instead of command-by-command from the client),
|
||||
this functionality has been moved to a plugin. Here's what to do to keep using it:
|
||||
!!! info "Actions Needed"
|
||||
|
||||
1. Install the plugin: `appium plugin install execute-driver`
|
||||
2. Ensure you start the Appium server with access to run the plugin by including it in the list of
|
||||
plugins designated on the command line, e.g., `appium --use-plugins=execute-driver`
|
||||
Check your Appium client documentation for the methods using these endpoints, and adjust your
|
||||
code to only use the accepted parameters
|
||||
|
||||
### :bangbang: External Files No Longer Supported for `--nodeconfig`, `--default-capabilities`, `--allow-insecure` and `--deny-insecure`
|
||||
### Internal Packages Renamed
|
||||
|
||||
These options can be provided as strings on the command line (a JSON string for `--nodeconfig` and
|
||||
a comma-separated list of strings for `--allow-insecure` and `--deny-insecure`). Arguments provided
|
||||
on the command line will likely need to be quoted or escaped.
|
||||
In Appium 1, the internal dependency packages were each located in their own repository. Appium 2
|
||||
moves to a monorepo structure and therefore renames many of these packages, for example:
|
||||
|
||||
The recommended method to provide these options is through a [configuration file](./config.md).
|
||||
```
|
||||
appium-base-driver # Appium 1
|
||||
@appium/base-driver # Appium 2
|
||||
```
|
||||
|
||||
In summary, if you are using a JSON Appium config file, you can simply cut-and-paste the contents
|
||||
of your "nodeconfig" JSON file into the value of the `server.nodeconfig` property. Any CSV-like
|
||||
files you had previously provided for `--allow-insecure` and `--deny-insecure` become the values
|
||||
of the `server.allow-insecure` and `server.deny-insecure` properties in the Appium config files
|
||||
(respectively); both are arrays of strings.
|
||||
!!! info "Actions Needed"
|
||||
|
||||
### :bangbang: Old drivers removed
|
||||
|
||||
The old iOS and Android (UiAutomator 1) drivers and related tools (e.g., `authorize-ios`) have been
|
||||
removed. They haven't been relevant for many years anyway.
|
||||
|
||||
### :bangbang: Server can no longer be started with `--port 0`
|
||||
|
||||
In Appium 1, it was possible to specify `--port 0` during server startup. This had the effect of
|
||||
starting Appium on a random free port. In Appium 2, port values must be `1` or higher. The random
|
||||
port assignment was never an intentional feature of Appium 1, but a consequence of how Node's
|
||||
HTTP servers work and the fact that there was no port input validation in Appium 1. If you want
|
||||
to find a random free port to start Appium on, you must now take care of this on your own prior to
|
||||
starting Appium. Starting Appium on an explicit and known port is the correct practice moving
|
||||
forward.
|
||||
|
||||
### :warning: Internal packages renamed
|
||||
|
||||
Some Appium-internal NPM packages have been renamed (for example, `appium-base-driver` is now
|
||||
`@appium/base-driver`). This is not a breaking change for Appium users, only for people who have
|
||||
built software that directly incorporates Appium's code.
|
||||
|
||||
### :warning: `wd` JavaScript client library no longer supported
|
||||
|
||||
For many years, some of Appium's authors maintained the [WD](https://github.com/admc/wd) client
|
||||
library. This library has been deprecated and has not been updated for use with the W3C WebDriver
|
||||
protocol. As such, if you're using this library you'll need to move to a more modern one. We
|
||||
recommend [WebdriverIO](https://webdriver.io).
|
||||
|
||||
### :warning: Appium Desktop replaced with Appium Inspector
|
||||
|
||||
The inspector functionality of Appium Desktop has been moved to its own app:
|
||||
[Appium Inspector](https://github.com/appium/appium-inspector). It is fully compatible with
|
||||
standalone Appium 2 servers, but also works with later versions of Appium 1 servers. Appium Desktop
|
||||
itself has been deprecated and is not compatible with Appium 2.
|
||||
|
||||
In addition to the app, Appium Inspector also has a browser version, accessible at
|
||||
[inspector.appiumpro.com](https://inspector.appiumpro.com). Note that in order to use the
|
||||
browser version with a local Appium server, you'll need to first start the server with the
|
||||
`--allow-cors` flag.
|
||||
If you do not directly import Appium packages into your code - none! However, if you do, make
|
||||
sure to update the names of your Appium package imports!
|
||||
|
||||
## Major New Features
|
||||
|
||||
Apart from the breaking changes mentioned above, in this section is a list of some of the major new
|
||||
features you may wish to take advantage of with Appium 2.
|
||||
Apart from the breaking changes mentioned above, here are some of the major new features you may
|
||||
wish to take advantage of with Appium 2:
|
||||
|
||||
### Plugins
|
||||
### Third-Party Drivers and Plugins
|
||||
|
||||
#### :tada: Server Plugins
|
||||
You are no longer limited to official drivers or plugins, or ones that the Appium team even knows
|
||||
about! Developers can now create their own custom drivers or plugins, which can be installed via
|
||||
Appium's [Extension CLI](../cli/extensions.md) from `npm`, `git`, GitHub, or even the local
|
||||
filesystem. Interested in building a driver or plugin? Check out the
|
||||
[Building Drivers](../developing/build-drivers.md) and
|
||||
[Building Plugins](../developing/build-plugins.md) guides.
|
||||
|
||||
Appium extension authors can now develop their own server plugins, which can intercept and modify
|
||||
any Appium command, or even adjust the way the underlying Appium HTTP server itself works. To learn
|
||||
more about plugins, read the new [Appium Introduction](../intro/index.md). Interested in building
|
||||
a plugin? Check out the [Building Plugins](../developing/build-plugins.md) guide.
|
||||
### Configuration Files
|
||||
|
||||
### :tada: Install drivers and plugins from anywhere
|
||||
|
||||
You're no longer limited to the drivers that come with Appium, or that the Appium team even knows
|
||||
about! Appium extension authors can now develop custom drivers, which can be downloaded or
|
||||
installed via Appium's [Extension CLI](../cli/extensions.md) from `npm`, `git`, GitHub, or even the
|
||||
local filesystem. Interested in building a driver? Check out the [Building
|
||||
Drivers](../developing/build-drivers.md) guide.
|
||||
|
||||
### :tada: Configuration Files
|
||||
|
||||
Appium now supports _configuration files_ in addition to command-line arguments. In a nutshell,
|
||||
nearly all arguments which Appium 1 required to be provided on the CLI are now able to be expressed
|
||||
via a configuration file. Configuration files may be in JSON, JS, or YAML format. See the
|
||||
[Config Guide](./config.md) for a full explanation.
|
||||
Appium now supports _configuration files_ in addition to command-line arguments. Nearly all options
|
||||
or flags that had to be specified on the CLI in Appium 1, can now also be provided in a
|
||||
configuration file. The file can be in JSON, JS, or YAML format. For more information, refer to
|
||||
the [Config File Guide](./config.md).
|
||||
|
||||
## Special Notes for Cloud Providers
|
||||
|
||||
The rest of this document has applied to Appium generally, but some of the architectural changes in
|
||||
Appium 2 will constitute breaking changes for Appium-related service providers, whether a
|
||||
cloud-based Appium host or an internal service. At the end of the day, the maintainer of the Appium
|
||||
server is responsible for installing and making available the various Appium drivers and plugins
|
||||
that end users may wish to use.
|
||||
Most of this guide has applied to Appium end users or developers, but some of the architectural
|
||||
changes in Appium 2 will constitute breaking changes for different Appium service providers. At the
|
||||
end of the day, the maintainer of the Appium server is responsible for installing and exposing the
|
||||
various Appium drivers and plugins that end users may wish to use.
|
||||
|
||||
We encourage cloud providers to thoroughly read and understand our [recommendation for cloud
|
||||
provider capabilities](./caps.md#special-notes-for-cloud-providers) in order to support user needs in
|
||||
|
||||
@@ -12,13 +12,6 @@ title: Welcome
|
||||
}
|
||||
</style>
|
||||
|
||||
<div style="text-align:center;width:100%;background-color:#ec366d;margin-top:-3em;margin-bottom:2em;">
|
||||
<h3 style="color:#ffffff;padding: 8px;">
|
||||
AppiumConf 2025 is partnering with SeleniumConf<br/> <span style="font-size:0.8em;">March 26-28 in Valencia, Spain. Join us to learn and connect with the community!</span><br/>
|
||||
<a style="color:#fcba12" href="https://seleniumconf.com/" target="_blank">Register Now</a>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<img src="assets/images/appium-logo-horiz.png" style="max-width: 400px;" />
|
||||
</div>
|
||||
|
||||
@@ -46,6 +46,7 @@ nav:
|
||||
- commands/execute-driver-plugin.md
|
||||
- commands/images-plugin.md
|
||||
- commands/relaxed-caps-plugin.md
|
||||
- commands/storage-plugin.md
|
||||
- commands/universal-xml-plugin.md
|
||||
- Guides:
|
||||
- Migration:
|
||||
@@ -56,6 +57,7 @@ nav:
|
||||
- guides/config.md
|
||||
- guides/security.md
|
||||
- guides/log-filters.md
|
||||
- guides/headers.md
|
||||
- guides/grid.md
|
||||
- guides/caching.md
|
||||
- guides/tls.md
|
||||
|
||||
@@ -180,45 +180,31 @@ Appium会使用`appiumCliTransformer: 'csv'`。
|
||||
|
||||
不需要这种特殊处理的属性直接使用`ajv`进行验证。这是如何工作的一些解释,所以接下来就是这样。
|
||||
|
||||
#### Validation of Individual Arguments via `ajv`
|
||||
#### 通过 `ajv` 实现单个参数的验证
|
||||
|
||||
When we think of a JSON schema, we tend to think, "I have this JSON file and I want to validate it
|
||||
against the schema". That's valid, and in fact Appium does just that with config files! However,
|
||||
Appium does not do this when validating arguments.
|
||||
当我们思考 JSON 模式时,通常会认为“我有一个 JSON 文件需要根据模式进行验证”。这种理解是正确的,实际上 Appium 在处理配置文件时正是这样做的!不过,Appium 在验证参数时采用了不同的方式。
|
||||
|
||||
!!! 注意
|
||||
|
||||
During implementation, I was tempted to mash all of the arguments together into
|
||||
a config-file-like data structure and then validate it all at once. I think that would have
|
||||
been _possible_, but since an object full of CLI arguments is a flat key/value structure and
|
||||
the schema is not, this seemed like trouble.
|
||||
在实现过程中,我曾试图将所有参数组合成一个类似配置文件的数据结构进行统一验证。虽然这在理论上是可行的,但由于 CLI 参数对象是扁平的键/值结构,而模式本身并非扁平结构,这种做法可能会引发问题。
|
||||
|
||||
Instead, Appium validates a value against a specific property _within_ the schema. To do this, it
|
||||
maintains a mapping between a CLI argument definition and its corresponding property. The mapping
|
||||
itself is a `Map` with a unique identifier for the argument as the key, and an `ArgSpec`
|
||||
(`lib/schema/arg-spec.js`) object as the value.
|
||||
Appium 实际采用的方式是:针对模式中特定的属性对参数值进行验证。为此,系统维护了 CLI 参数定义与对应模式属性之间的映射关系。这个映射关系通过一个 `Map` 实现,其中键是参数的唯一标识符,值是一个 `ArgSpec` 对象(定义于 `lib/schema/arg-spec.js`)。
|
||||
|
||||
An `ArgSpec` object stores the following metadata:
|
||||
`ArgSpec` 对象包含以下元数据:
|
||||
|
||||
| Property Name | Description |
|
||||
| --------------- | ------------------------------------------------------------------------------------- |
|
||||
| `name` | Canonical name of the argument, corresponding to the property name in the schema. |
|
||||
| `extType?` | `driver` or `plugin`, if appropriate |
|
||||
| `extName?` | Extension name, if appropriate |
|
||||
| `ref` | Computed `$id` of the property in the schema |
|
||||
| `arg` | Argument as accepted on CLI, without leading dashes |
|
||||
| `dest` | Property name in parsed arguments object (as returned by `argparse`'s `parse_args()`) |
|
||||
| `defaultValue?` | Value of the `default` keyword in schema, if appropriate |
|
||||
| 属性名称 | 描述 |
|
||||
| --------------- | --------------------------------------------------------------------------------------- |
|
||||
| `name` | 参数的规范名称,对应模式中的属性名 |
|
||||
| `extType?` | 扩展类型(`driver` 或 `plugin`),如果适用 |
|
||||
| `extName?` | 扩展名称,如果适用 |
|
||||
| `ref` | 模式中该属性的计算后 `$id` 引用 |
|
||||
| `arg` | CLI 接受的参数名(不带前导短横线) |
|
||||
| `dest` | 解析后参数对象的属性名(由 `argparse` 的 `parse_args()` 返回) |
|
||||
| `defaultValue?` | 模式中 `default` 关键字对应的值(如果存在) |
|
||||
|
||||
When a schema is [finalized](#schema-finalization), the `Map` is populated with `ArgSpec` objects
|
||||
for all known arguments.
|
||||
当模式完成[最终化](#schema-finalization)时,该 `Map` 会被填充所有已知参数的 `ArgSpec` 对象。
|
||||
|
||||
So when the adapter is creating the pipeline of functions for the argument's `type`, it already has
|
||||
an `ArgSpec` for the argument. It creates a function which calls `validate(value, ref)` (in
|
||||
`lib/schema/schema.js`) where `value` is whatever the user provided, and `ref` is the `ref`
|
||||
property of the `ArgSpec`. The concept is that `ajv` can validate using _any_ `ref` it knows about;
|
||||
each property in a schema can be referenced by this `ref` whether it's defined or not. To help
|
||||
visualize, if a schema is:
|
||||
在适配器为参数的 `type` 创建函数管道时,系统已经拥有该参数的 `ArgSpec`。它会创建一个调用 `validate(value, ref)` 的函数(位于 `lib/schema/schema.js`),其中 `value` 是用户提供的参数值,`ref` 是 `ArgSpec` 的 `ref` 属性。核心原理在于 `ajv` 可以使用任何已知的 `ref` 进行验证——无论该属性是否被显式定义,模式中的每个属性都可以通过其 `ref` 被引用。以下列模式为例:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -232,76 +218,50 @@ visualize, if a schema is:
|
||||
}
|
||||
```
|
||||
|
||||
The `ref` of `foo` would be `my-schema.json#/properties/foo`. Assuming our `Ajv` instance knows
|
||||
about this `my-schema.json`, then we can call its `getSchema(ref)` method (which has a `schema`
|
||||
property, but is a misnomer nonetheless) to get a validation function; `validate(value, ref)` in
|
||||
`schema.js` calls this validation function.
|
||||
其中 `foo` 的 `ref` 应为 `my-schema.json#/properties/foo`。假设我们的 `Ajv` 实例已注册该模式,即可通过 `getSchema(ref)` 方法(虽然名称有误,但其 `schema` 属性有效)获取验证函数;`schema.js` 中的 `validate(value, ref)` 方法正是调用此验证函数。
|
||||
|
||||
!!! 注意
|
||||
|
||||
The schema spec says a schema author can supply an explicit `$id` keyword to override this;
|
||||
it's unsupported by Appium at this time. If needed, extension authors must carefully use `$ref`
|
||||
without custom `$id`s. It's highly unlikely an extension would have a schema so complicated as
|
||||
to need this, however; Appium itself doesn't even use `$ref` to define its own properties!
|
||||
虽然模式规范允许通过显式声明 `$id` 来覆盖默认引用标识,但 Appium 目前不支持此特性。如有需要,扩展开发者必须谨慎使用不包含自定义 `$id` 的 `$ref` 引用。不过 Appium 自身的模式定义都未使用 `$ref`,可见扩展通常不需要如此复杂的模式结构。
|
||||
|
||||
Next, let's take a look at how Appium loads schemas. This actually happens _before_ any argument
|
||||
validation.
|
||||
接下来我们将探讨 Appium 如何加载模式。值得注意的是,模式加载过程实际上发生在参数验证之前。
|
||||
|
||||
## Schema Loading
|
||||
## 模式加载
|
||||
|
||||
Let's ignore extensions for a moment, and start with the base schema.
|
||||
让我们暂时忽略扩展功能,先从基础模式开始解析。
|
||||
|
||||
When something first imports the `lib/schema/schema.js` module, an instance of an `AppiumSchema` is
|
||||
created. This is a singleton, and its methods are exported from the module (all of which are bound
|
||||
to the instance).
|
||||
当首次导入 `lib/schema/schema.js` 模块时,系统会创建一个 `AppiumSchema` 实例。该实例为单例模式,其所有方法(均已绑定到实例)都通过该模块对外暴露。
|
||||
|
||||
The constructor does very little; it instantiates an `Ajv` instance and configures it with Appium's
|
||||
[custom keywords](#custom-keyword-reference) and adds support for the `format` keyword via the
|
||||
[ajv-formats](https://npm.im/ajv-formats) module.
|
||||
构造函数仅执行基础操作:实例化一个 `Ajv` 验证器实例,并通过以下方式对其进行配置:添加 Appium 的[自定义关键字](#custom-keyword-reference);
|
||||
通过 [ajv-formats](https://npm.im/ajv-formats) 模块启用 `format` 关键字支持。
|
||||
|
||||
Otherwise, the `AppiumSchema` instance does not interact with the `Ajv` instance until its
|
||||
`finalize()` method (exported as `finalizeSchema()`) is called. When this method is called, we're
|
||||
saying "we are not going to add any more schemas; go ahead and create `ArgSpec` objects and
|
||||
register schemas with `ajv`".
|
||||
在调用 `finalize()` 方法(导出为 `finalizeSchema()`)之前,`AppiumSchema` 实例不会与 `Ajv` 实例产生交互。此方法的调用标志着"模式定义阶段结束,将开始创建 `ArgSpec` 对象并向 `ajv` 注册最终模式"。
|
||||
|
||||
When does finalization happen? Well:
|
||||
最终化触发的时机如下:
|
||||
|
||||
1. When the `appium` executable begins, it _checks for and configures extensions_ (hand-wave) in `APPIUM_HOME`.
|
||||
2. Only then does it start to think about arguments--it instantiates an `ArgParser`, which (as you'll recall) runs the adapter to convert the schema to arguments.
|
||||
3. _Finalization happens here_--when creating the parser. Appium need the schema(s) to be registered with `ajv` in order to create validation functions for arguments.
|
||||
4. Thereafter, Appium parses the arguments with the `ArgParser`.
|
||||
5. Finally, decides what to do with the returned object.
|
||||
1. 当 `appium` 可执行文件启动时,首先在 `APPIUM_HOME` 目录中检查并配置扩展(此处简化处理)
|
||||
2. 随后开始处理参数——实例化 `ArgParser`,该操作会运行适配器将模式转换为参数定义
|
||||
3. 最终化在此处发生:创建解析器时,Appium 需要向 `ajv` 注册模式以生成参数验证函数
|
||||
4. 通过 `ArgParser`完成参数解析
|
||||
5. 最终根据解析结果决定后续操作流程
|
||||
|
||||
Without extensions, `finalize()` still knows about the Appium base schema
|
||||
(`appium-config-schema.js`), and just registers that. However, step 1. above is doing a _lot of
|
||||
work_, so let's look at how extensions come into play.
|
||||
即使没有扩展功能,`finalize()` 仍会识别 Appium 的基础模式(`appium-config-schema.js`)并进行注册。不过上述步骤 1 实际涉及复杂的扩展处理机制,接下来我们将深入探讨扩展功能的影响。
|
||||
|
||||
## Extension Support
|
||||
## 扩展支持
|
||||
|
||||
One of the design goals of this system is the following:
|
||||
本系统的设计目标之一是:
|
||||
|
||||
_An extension should be able to register custom CLI arguments with the Appium, and a user should be
|
||||
able to use them like any other argument_.
|
||||
扩展应该能够向Appium注册自定义CLI参数,用户应该能够像使用其他参数一样使用它们。
|
||||
|
||||
Previously, Appium 2 accepted arguments in this manner (via `--driverArgs`), but validation was
|
||||
hand-rolled and required extension implementors to use a custom API. It also required the user to
|
||||
awkwardly pass a JSON string as the configuration on the command-line. Further, no contextual help
|
||||
(via `--help`) existed for these arguments.
|
||||
此前,Appium 2通过`--driverArgs`方式接受参数,但验证过程需要手动实现,并要求扩展开发者使用自定义API。这种方式还要求用户在命令行中笨拙地传递JSON字符串作为配置。此外,这些参数没有上下文帮助信息(通过`--help`)。
|
||||
|
||||
Now, by providing a schema for its options, a driver or plugin can register CLI arguments and
|
||||
config file schemas with Appium.
|
||||
现在,通过为选项提供模式(schema),驱动或插件可以向Appium注册CLI参数和配置文件模式。
|
||||
|
||||
To register a schema, an extension must provide the `appium.schema` property in its `package.json`.
|
||||
The value may be a schema or a path to a schema. If the latter, the schema should be JSON or
|
||||
a CommonJS module (ESM not supported at this time, nor is YAML).
|
||||
要注册模式,扩展必须在其`package.json`中提供`appium.schema`属性。该值可以是模式本身或模式文件的路径。如果是路径,模式文件应为JSON或CommonJS模块(目前不支持ESM和YAML)。
|
||||
|
||||
For any property in this schema, the property will appear as a CLI argument of the form
|
||||
`--<extension-type>-<extension-name>-<property-name>`. For example, if the `fake` driver provides
|
||||
a property `foo`, the argument will be `--driver-fake-foo`, and will show in `appium server --help`
|
||||
like any other CLI argument.
|
||||
对于该模式中的任何属性,都将以`--<扩展类型>-<扩展名称>-<属性名称>`的形式作为CLI参数出现。例如,如果`fake`驱动提供了`foo`属性,对应的参数将是`--driver-fake-foo`,并会像其他CLI参数一样显示在`appium server --help`中。
|
||||
|
||||
The corresponding property in a config file would be
|
||||
`server.<extension-type>.<extension-name>.<property-name>`, e.g.:
|
||||
配置文件中对应的属性路径为`server.<扩展类型>.<扩展名称>.<属性名称>`,例如:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -315,47 +275,35 @@ The corresponding property in a config file would be
|
||||
}
|
||||
```
|
||||
|
||||
The naming convention described above avoids problems of one extension type having a name conflict
|
||||
with a different extension type.
|
||||
上述命名约定避免了不同扩展类型之间的名称冲突问题。
|
||||
|
||||
!!! 注意
|
||||
|
||||
While an extension can provide aliases via `appiumCliAliases`, "short" flags are disallowed,
|
||||
since all arguments from extensions are prefixed with `--<extension-type>-<extension-name>-`.
|
||||
The extension name and argument name will be kebab-cased for the CLI, according to [Lodash's
|
||||
rules](https://lodash.com/docs/4.17.15#kebabCase) around kebab-casing.
|
||||
虽然扩展可以通过`appiumCliAliases`提供别名,但禁止使用"短"标志(short flags),因为所有扩展参数都带有`--<扩展类型>-<扩展名称>-`前缀。根据[Lodash的kebab-case规则](https://lodash.com/docs/4.17.15#kebabCase),扩展名称和参数名称在CLI中将被转换为kebab-case格式。
|
||||
|
||||
The schema object will look much like Appium's base schema, but it will only have top-level
|
||||
properties (nested properties are currently unsupported). Example:
|
||||
模式对象的结构与Appium基础模式相似,但仅支持顶层属性(目前不支持嵌套属性)。示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "my rad schema for the cowabunga driver",
|
||||
"title": "为cowabunga驱动设计的超棒模式",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fizz": {
|
||||
"type": "string",
|
||||
"default": "buzz",
|
||||
"$comment": "corresponds to CLI --driver-cowabunga-fizz"
|
||||
"$comment": "对应CLI参数--driver-cowabunga-fizz"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As written in a user's config file, this would be the `server.driver.cowabunga.fizz` property.
|
||||
在用户的配置文件中,该属性将表示为`server.driver.cowabunga.fizz`。
|
||||
|
||||
When extensions are loaded, the `schema` property is verified and the schema is registered with the
|
||||
`AppiumSchema` (it is _not_ registered with `Ajv` until `finalize()` is called).
|
||||
当扩展被加载时,`schema`属性会被验证,并且模式会被注册到`AppiumSchema`中(在调用`finalize()`之前不会注册到`Ajv`)。
|
||||
|
||||
During finalization, each registered schema is added to the `Ajv` instance. The schema is assigned
|
||||
an `$id` based on the extension type and name (which overrides whatever the extension provides, if
|
||||
anything). Schemas are also forced to disallowed unknown arguments via the `additionalProperties:
|
||||
false` keyword.
|
||||
在最终化阶段,每个注册的模式都会被添加到`Ajv`实例中。模式会根据扩展类型和名称分配一个`$id`(这会覆盖扩展可能提供的任何现有ID)。模式还会通过`additionalProperties: false`强制禁止未知参数。
|
||||
|
||||
Behind the scenes, the base schema has `driver` and `plugin` properties which are objects. When
|
||||
finalized, a property is added to each--corresponding to an extension name--and the value of this
|
||||
property is a reference to the `$id` of a property in the extension schema. For example, the
|
||||
`server.driver` property will look like this:
|
||||
在底层实现中,基础模式包含`driver`和`plugin`属性(都是对象类型)。最终化时,会为每个属性添加对应扩展名称的子属性,其值是对扩展模式中`$id`的引用。例如,`server.driver`属性将如下所示:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -367,45 +315,30 @@ property is a reference to the `$id` of a property in the extension schema. For
|
||||
}
|
||||
```
|
||||
|
||||
This is why we call it the "base" schema--it is _mutated_ when extensions provide schemas. The
|
||||
extension schemas are kept separately, but the _references_ are added to the schema before it's
|
||||
ultimately added to `ajv`. This works because an `Ajv` instance understands references _from_ any
|
||||
schema it knows about _to_ any schema it knows about.
|
||||
这就是为什么我们称之为“基础”模式——当扩展提供模式时,它会发生变化。扩展模式是分开保存的,但在最终将其添加到`ajv`之前,会将引用添加到模式中。这是有效的,因为`Ajv`实例能够理解已注册模式之间的相互引用。
|
||||
|
||||
!!! 注意
|
||||
|
||||
This makes it impossible to provide a complete static schema for Appium _and_ the installed
|
||||
extensions (as of Nov 5 2021). A static `.json` schema _is_ generated from the base (via a Gulp
|
||||
task), but it does not contain any extension schemas. The static schema also has uses beyond
|
||||
Appium; e.g., IDEs can provide contextual error-checking of config files this way. Let's solve
|
||||
this?
|
||||
这使得为Appium及其安装的扩展提供完整的静态模式变得不可能(截至2021年11月5日)。基础模式会通过Gulp任务生成静态`.json`模式文件,但不包含任何扩展模式。这种静态模式还有其他用途,例如IDE可以通过这种方式提供配置文件的上下文错误检查。这个问题需要后续解决?
|
||||
|
||||
Just like how we look up the reference ID of a particular argument in the base schema, validation
|
||||
of arguments from extensions happens the exact same way. If the `cowabunga` driver has the schema
|
||||
ID `driver-cowabunga.json`, then the `fizz` property can be referenced from any schema registered
|
||||
with `ajv` via `driver-cowabunga.json#/properties/fizz`. "Base" schema arguments begin with
|
||||
`appium.json#properties/` instead.
|
||||
与在基础模式中查找参数的引用ID方式相同,扩展参数的验证过程完全一致。如果`cowabunga`驱动的模式ID是`driver-cowabunga.json`,那么`fizz`属性可以通过`driver-cowabunga.json#/properties/fizz`路径在任意已注册模式中被引用。“基础”模式参数的引用路径则以`appium.json#properties/`开头。
|
||||
|
||||
## Development Environment Support
|
||||
## 开发环境支持
|
||||
|
||||
During the flow of development, a couple extra tasks have been automated to maintain the base
|
||||
schema:
|
||||
在开发流程中,我们自动化了以下几个任务来维护基础模式:
|
||||
|
||||
- As a post-transpilation step, a `lib/appium-config.schema.json` gets generated from
|
||||
- `lib/schema/appium-config-schema.js` (in addition to its CJS counterpart generated by Babel).
|
||||
- This file is under version control. It ends up being _copied_ to
|
||||
- `build/lib/appium-config.schema.json` in this step. A pre-commit hook (see
|
||||
- `scripts/generate-schema-declarations.js` in the root monorepo) generates
|
||||
- a `types/appium-config-schema.d.ts` from the above JSON file. The types in `types/types.d.ts`
|
||||
- depend upon this file. This file is under version control.
|
||||
- 作为后置转译步骤,会从 `lib/schema/appium-config-schema.js` 生成 `lib/appium-config.schema.json`(除了 Babel 生成的 CJS 对应文件外)
|
||||
- 该文件受版本控制管理。在此步骤中会被复制到 `build/lib/appium-config.schema.json`
|
||||
- 通过预提交钩子(参见 monorepo 根目录的 `scripts/generate-schema-declarations.js`)会从上述 JSON 文件生成 `types/appium-config-schema.d.ts`
|
||||
- `types/types.d.ts` 中的类型定义依赖此文件,该文件受版本控制管理
|
||||
|
||||
## Custom Keyword Reference
|
||||
## 自定义关键字参考
|
||||
|
||||
Keywords are defined in `lib/schema/keywords.js`.
|
||||
关键字定义于 `lib/schema/keywords.js`
|
||||
|
||||
- `appiumCliAliases`: allows a schema to express aliases (e.g., a CLI argument can be `--verbose` or `-v`). This is an array of strings. Strings shorter than three (3) characters will begin with a single dash (`-`) instead of a double-dash (`--`). Note that any argument provided by an extension will begin with a double-dash, because these are required to have the `--<extension-type>-<extension-name>-` prefix.
|
||||
- `appiumCliDest`: allows a schema to specify a custom property name in the post-`argprase` arguments objects. If not set, this becomes a camelCased string.
|
||||
- `appiumCliDescription`: allows a schema to override the description of the argument when displayed on the command-line. This is useful paired with `appiumCliTransformer` (or `array`/`object`-typed properties), since there's a substantial difference between what a CLI-using user can provide vs. what a config-file-using user can provide.
|
||||
- `appiumCliTransformer`: currently a choice between `csv` and `json`. These are custom functions which post-process a value. They are not used when loading & validating config files, but the idea should be that they result in the same object you'd get if you used whatever the config file wanted (e.g., an array of strings). `csv` is for comma-delimited strings and CSV files; `json` is for raw JSON strings and `.json` files.
|
||||
- `appiumCliIgnore`: If `true`, do not support this property on the CLI.
|
||||
- `appiumDeprecated`: If `true`, the property is considered "deprecated", and will be displayed as such to the user (e.g., in the `--help` output). Note the JSON Schema draft-2019-09 introduces a new keyword `deprecated` which we should use instead if upgrading to this metaschema. When doing so, `appiumDeprecated` should itself be marked as `deprecated`.
|
||||
- `appiumCliAliases`: 允许模式声明参数别名(例如 CLI 参数可以是 `--verbose` 或 `-v`)。值为字符串数组。短于三(3)个字符的字符串将使用单短横线(`-`)而非双短横线(`--`)开头。注意任何由扩展提供的参数都必须以双短横线开头,因为这些参数需要带有 `--<extension-type>-<extension-name>-` 前缀
|
||||
- `appiumCliDest`: 允许模式指定 `argparse` 解析后参数对象中的自定义属性名。若未设置,将转换为驼峰式命名
|
||||
- `appiumCliDescription`: 允许模式覆盖命令行显示的参数描述。与 `appiumCliTransformer`(或 `array`/`object` 类型属性)配合使用时非常有用,因为 CLI 用户可提供的内容与配置文件用户可提供的内容存在显著差异
|
||||
- `appiumCliTransformer`: 目前支持 `csv` 和 `json` 两种选项。这些是用于后处理值的自定义函数。在加载和验证配置文件时不使用这些转换器,但核心思想是它们应该产生与使用配置文件相同的对象(例如字符串数组)。`csv` 用于逗号分隔字符串和 CSV 文件;`json` 用于原始 JSON 字符串和 `.json` 文件
|
||||
- `appiumCliIgnore`: 若为 `true`,则不在 CLI 中支持该属性
|
||||
- `appiumDeprecated`: 若为 `true`,该属性将被视为"已弃用",并会向用户显示为弃用状态(例如在 `--help` 输出中)。注意 JSON Schema draft-2019-09 引入了新的 `deprecated` 关键字,如果我们升级到该元模式应该改用此关键字。迁移时,`appiumDeprecated` 本身应被标记为 `deprecated`
|
||||
@@ -43,6 +43,7 @@ appium driver install <驱动名称>
|
||||
|[Tizen](https://github.com/Samsung/appium-tizen-driver)|`--source=npm appium-tizen-driver`|Android|Native|Community / Samsung|
|
||||
|[TizenTV](https://github.com/headspinio/appium-tizen-tv-driver)|`--source=npm appium-tizen-tv-driver`|Samsung TV|Web|HeadSpin|
|
||||
|[Youi](https://github.com/YOU-i-Labs/appium-youiengine-driver)|`--source=npm appium-youiengine-driver`|iOS, Android, macOS, Linux, tvOS|Native|Community / You.i|
|
||||
|[NovaWindows](https://github.com/AutomateThePlanet/appium-novawindows-driver)|`--source=npm appium-novawindows-driver`|Windows|Native|Community / Automate The Planet|
|
||||
|
||||
!!! 注意
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export const KNOWN_PLUGINS = Object.freeze(
|
||||
images: '@appium/images-plugin',
|
||||
'execute-driver': '@appium/execute-driver-plugin',
|
||||
'relaxed-caps': '@appium/relaxed-caps-plugin',
|
||||
storage: '@appium/storage-plugin',
|
||||
'universal-xml': '@appium/universal-xml-plugin',
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "appium",
|
||||
"version": "2.16.2",
|
||||
"version": "2.18.0",
|
||||
"description": "Automation for Apps.",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -60,20 +60,20 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-driver": "^9.16.2",
|
||||
"@appium/base-plugin": "^2.3.3",
|
||||
"@appium/docutils": "^1.0.32",
|
||||
"@appium/logger": "^1.6.1",
|
||||
"@appium/base-driver": "^9.17.0",
|
||||
"@appium/base-plugin": "^2.3.6",
|
||||
"@appium/docutils": "^1.1.0",
|
||||
"@appium/logger": "^1.7.0",
|
||||
"@appium/schema": "^0.8.1",
|
||||
"@appium/support": "^6.0.6",
|
||||
"@appium/types": "^0.25.1",
|
||||
"@appium/support": "^6.1.0",
|
||||
"@appium/types": "^0.25.3",
|
||||
"@sidvind/better-ajv-errors": "3.0.1",
|
||||
"ajv": "8.17.1",
|
||||
"ajv-formats": "3.0.1",
|
||||
"argparse": "2.0.1",
|
||||
"async-lock": "1.4.1",
|
||||
"asyncbox": "3.0.0",
|
||||
"axios": "1.8.1",
|
||||
"axios": "1.8.4",
|
||||
"bluebird": "3.7.2",
|
||||
"lilconfig": "3.1.3",
|
||||
"lodash": "4.17.21",
|
||||
@@ -84,11 +84,11 @@
|
||||
"semver": "7.7.1",
|
||||
"source-map-support": "0.5.21",
|
||||
"teen_process": "2.3.1",
|
||||
"type-fest": "4.36.0",
|
||||
"type-fest": "4.40.0",
|
||||
"winston": "3.17.0",
|
||||
"wrap-ansi": "7.0.0",
|
||||
"ws": "8.18.1",
|
||||
"yaml": "2.7.0"
|
||||
"yaml": "2.7.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"webdriverio": "9.10.1"
|
||||
"webdriverio": "9.12.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ describe('FakeDriver via HTTP', function () {
|
||||
should.exist(driver.sessionId);
|
||||
driver.sessionId.should.be.a('string');
|
||||
await driver.deleteSession();
|
||||
await driver.getTitle().should.eventually.be.rejectedWith(/terminated/);
|
||||
await driver.getTitle().should.eventually.be.rejected;
|
||||
});
|
||||
|
||||
it('should be able to run two FakeDriver sessions simultaneously', async function () {
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [9.17.0](https://github.com/appium/appium/compare/@appium/base-driver@9.16.4...@appium/base-driver@9.17.0) (2025-04-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add a possibility to mask sensitive log values depending on request headers ([#21123](https://github.com/appium/appium/issues/21123)) ([c24d2ac](https://github.com/appium/appium/commit/c24d2ac46123f41ee9b54e0adefacfabd149089c))
|
||||
* **base-driver:** Accept `x-request-id` as override to generated requestId in `handleLogContext` ([#21154](https://github.com/appium/appium/issues/21154)) ([a82476f](https://github.com/appium/appium/commit/a82476f2fcb4aa3a3c4660d96fdd261a98afa4af))
|
||||
|
||||
|
||||
|
||||
## [9.16.4](https://github.com/appium/appium/compare/@appium/base-driver@9.16.3...@appium/base-driver@9.16.4) (2025-03-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **base-driver:** Exclude proxied headers from the server response ([#21120](https://github.com/appium/appium/issues/21120)) ([f800c9d](https://github.com/appium/appium/commit/f800c9d760ab3271532c42a507fbaf2bf190fc4b))
|
||||
|
||||
|
||||
|
||||
## [9.16.3](https://github.com/appium/appium/compare/@appium/base-driver@9.16.2...@appium/base-driver@9.16.3) (2025-03-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **base-driver:** Fix proxy url generation ([#21099](https://github.com/appium/appium/issues/21099)) ([e68757b](https://github.com/appium/appium/commit/e68757b3493a5b0f961f7136c0ae6e857d806f09))
|
||||
* **base-driver:** Tune capabilities array parsing ([#21044](https://github.com/appium/appium/issues/21044)) ([594bc04](https://github.com/appium/appium/commit/594bc04c03fb073cd7ad31d7e23f77fb8041b92e))
|
||||
* **base-driver:** Update parseCapsArray function types ([#21045](https://github.com/appium/appium/issues/21045)) ([5541142](https://github.com/appium/appium/commit/554114203fbe26f303337f049f942e046c815074))
|
||||
|
||||
|
||||
|
||||
## [9.16.2](https://github.com/appium/appium/compare/@appium/base-driver@9.16.1...@appium/base-driver@9.16.2) (2025-02-20)
|
||||
|
||||
|
||||
|
||||
@@ -10,14 +10,6 @@ directly as it does nothing on its own. Instead, you should extend this driver w
|
||||
*own* Appium drivers. Check out the [Building Drivers](https://appium.io/docs/en/latest/developing/build-drivers/)
|
||||
documentation for more details.
|
||||
|
||||
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
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
appium-base-driver
|
||||
===================
|
||||
This is the parent class that all [appium](appium.io) drivers inherit from. Appium drivers themselves can either be started from the command line as standalone appium servers, or can be included by another module (appium) which then proxies commands to the appropriate driver based on [Desired Capabilities](https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md).
|
||||
|
||||
An appium driver is a module which processes [Mobile Json Wire Protocol](https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile) commands and controls a device accordingly. The commands can either come in over HTTP as json api requests, or they can be passed to the driver object programmatically as already-parsed json object (without the HTTP headers and junk).
|
||||
|
||||
The appium Base driver already includes the [mjsonwp](https://github.com/appium/appium-base-driver/blob/master/lib/mjsonwp/README.md) module, which is the HTTP server that converts incoming requests into json objects that get sent to the driver programmatically.
|
||||
|
||||
The appium Base driver already has all the REST api routes, validation, and error codes supplied by [mjsonwp](https://github.com/appium/appium-base-driver/blob/master/lib/mjsonwp/README.md).
|
||||
|
||||
Appium drivers are designed to have a *single testing session* per instantiation. This means that one Driver object should be attached to a single device and handle commands from a single client. The main appium driver handles multiple sessions and instantiates a new instance of the desired driver for each new session.
|
||||
|
||||
## Writing your own appium driver
|
||||
|
||||
Writing your own appium driver starts with inheriting and extending this Base driver module.
|
||||
|
||||
Appium Base driver has some properties that all drivers share:
|
||||
|
||||
- `driver.opts` - these are the options passed into the driver constructor. Your driver's constructor should take an object of options and pass it on the the Base driver by calling `super(opts)` in your constructor.
|
||||
|
||||
- `driver.desiredCapConstraints` - Base driver sets this property with a customer `setter` function so that when you create a driver, you can add an object which defines the validation contraints of which desired capabilities your new driver can handle. Of course each driver will have it's own specific desired capabilities. Look for examples on our other drivers.
|
||||
|
||||
- `driver.createSession(caps)` - this is the function which gets desired capabilities and creates a session. Make sure to call `super.createSession(caps)` so that things like `this.sessionId` and `this.caps` are populated, and the caps are validated against your `desiredCapConstraints`.
|
||||
|
||||
- `driver.caps` - these are the desired capabilities for the current session.
|
||||
|
||||
- `driver.sessionId` - this is the ID of the current session. It gets populated automaticall by `baseDriver.createSession`.
|
||||
|
||||
- `driver.proxyReqRes()` - used by mjsonwp module for proxying http commands to another process (like chromedriver or selendroid)
|
||||
|
||||
- `driver.jwpProxyAvoid` - used by mjsonwp module. You can specify what REST api routes which you want to SKIP the automatic proxy to another server (which is optional) and instead be handled by your driver.
|
||||
|
||||
|
||||
Base driver exposes an event called `onUnexpectedShutdown` which is called when the driver is shut down unexpectedly (usually after invocation of the `startUnexpectedShutdown` method).
|
||||
|
||||
Your driver should also implement a startUnexpectedShutdown method?
|
||||
@@ -1,59 +0,0 @@
|
||||
## appium-express
|
||||
|
||||
[Express](http://expressjs.com/) server tuned for to serve [Appium](http://appium.io/).
|
||||
|
||||
|
||||
### Configuration
|
||||
|
||||
The `appium-express` server comes configured with:
|
||||
|
||||
1. appropriate logging formats
|
||||
2. service of necessary static assets
|
||||
3. allowance of cross-domain requests
|
||||
4. default error handling
|
||||
5. fix for invalid content types sent by certain clients
|
||||
|
||||
To configure routes, a function that takes an Express server is passed into the
|
||||
server. This function can add whatever routes are wanted.
|
||||
|
||||
|
||||
### Usage
|
||||
|
||||
```js
|
||||
import { server } from 'appium-base-driver';
|
||||
|
||||
|
||||
// configure the routes
|
||||
function configureRoutes (app) {
|
||||
app.get('/hello', (req, res) => {
|
||||
res.header['content-type'] = 'text/html';
|
||||
res.status(200).send('Hello');
|
||||
});
|
||||
app.get('/world', (req, res) => {
|
||||
res.header['content-type'] = 'text/html';
|
||||
res.status(200).send('World');
|
||||
});
|
||||
}
|
||||
|
||||
const port = 5000;
|
||||
const host = 'localhost';
|
||||
|
||||
const appiumServer = await server({
|
||||
routeConfiguringFunction,
|
||||
port,
|
||||
host,
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
## Watch
|
||||
|
||||
```
|
||||
npm run watch
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
@@ -78,12 +78,17 @@ export function fixPythonContentType(basePath) {
|
||||
* @returns {any}
|
||||
*/
|
||||
export function handleLogContext(req, res, next) {
|
||||
const requestId = util.uuidV4();
|
||||
const requestId = fetchHeaderValue(req, 'x-request-id') || util.uuidV4();
|
||||
|
||||
const sessionId = SESSION_ID_PATTERN.exec(req.url)?.[1];
|
||||
const sessionInfo = sessionId ? {sessionId, sessionSignature: calcSignature(sessionId)} : {};
|
||||
const isSensitiveHeaderValue = fetchHeaderValue(req, 'x-appium-is-sensitive');
|
||||
|
||||
log.updateAsyncContext({requestId, ...sessionInfo}, true);
|
||||
log.updateAsyncContext({
|
||||
requestId,
|
||||
...sessionInfo,
|
||||
isSensitive: ['true', '1', 'yes'].includes(_.toLower(isSensitiveHeaderValue)),
|
||||
}, true);
|
||||
|
||||
return next();
|
||||
}
|
||||
@@ -181,6 +186,11 @@ export function catch404Handler(req, res) {
|
||||
|
||||
const SESSION_ID_PATTERN = /\/session\/([^/]+)/;
|
||||
|
||||
/**
|
||||
* @param {import('express').Request} req
|
||||
* @param {any} body
|
||||
* @returns {any}
|
||||
*/
|
||||
function patchWithSessionId(req, body) {
|
||||
const match = SESSION_ID_PATTERN.exec(req.url);
|
||||
if (match) {
|
||||
@@ -188,3 +198,14 @@ function patchWithSessionId(req, body) {
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('express').Request} req
|
||||
* @param {string} name
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
function fetchHeaderValue(req, name) {
|
||||
return _.isArray(req.headers[name])
|
||||
? req.headers[name][0]
|
||||
: req.headers[name];
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
## appium-jsonwp-proxy
|
||||
|
||||
Proxy middleware for the Selenium [JSON Wire Protocol](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md). Allows
|
||||
|
||||
|
||||
### Usage
|
||||
|
||||
The proxy is used by instantiating with the details of the Selenium server to which to proxy. The options for the constructor are passed as a hash with the following possible members:
|
||||
|
||||
- `scheme` - defaults to 'http'
|
||||
- `server` - defaults to 'localhost'
|
||||
- `port` - defaults to `4444`
|
||||
- `base` - defaults to ''
|
||||
- `sessionId` - the session id of the session on the remote server
|
||||
- `reqBasePath` - the base path of the server which the request was originally sent to (defaults to '')
|
||||
|
||||
Once the proxy is created, there are two `async` methods:
|
||||
|
||||
`command (url, method, body)`
|
||||
|
||||
Sends a "command" to the proxied server, using the "url", which is the endpoing, with the HTTP method and optional body.
|
||||
|
||||
```js
|
||||
import { JWProxy } from 'appium-base-driver';
|
||||
|
||||
let host = 'my.host.com';
|
||||
let port = 4445;
|
||||
|
||||
let proxy = new JWProxy({server: host, port: port});
|
||||
|
||||
// get the Selenium server status
|
||||
let seStatus = await proxy.command('/status', 'GET');
|
||||
```
|
||||
|
||||
`proxyReqRes (req, res)`
|
||||
|
||||
Proxies a request and response to the proxied server. Used to handle the entire conversation of a request/response cycle.
|
||||
|
||||
```js
|
||||
import { JWProxy } from 'appium-base-driver';
|
||||
import http from 'http';
|
||||
|
||||
let host = 'my.host.com';
|
||||
let port = 4445;
|
||||
|
||||
let proxy = new JWProxy({server: host, port: port});
|
||||
|
||||
|
||||
http.createServer(function (req, res) {
|
||||
await proxy.proxyReqRes(res, res);
|
||||
}).listen(9615);
|
||||
```
|
||||
@@ -428,18 +428,13 @@ export class JWProxy {
|
||||
/** @type {import('@appium/types').HTTPMethod} */ (req.method),
|
||||
req.body
|
||||
);
|
||||
for (const [name, value] of _.toPairs(response.headers)) {
|
||||
if (!_.isNil(value)) {
|
||||
res.setHeader(name, _.isBoolean(value) ? String(value) : value);
|
||||
}
|
||||
}
|
||||
statusCode = response.statusCode;
|
||||
} catch (err) {
|
||||
[statusCode, resBodyObj] = getResponseForW3CError(
|
||||
isErrorType(err, errors.ProxyRequestError) ? err.getActualError() : err
|
||||
);
|
||||
}
|
||||
res.set('content-type', 'application/json; charset=utf-8');
|
||||
res.setHeader('content-type', 'application/json; charset=utf-8');
|
||||
if (!_.isPlainObject(resBodyObj)) {
|
||||
const error = new errors.UnknownError(
|
||||
`The downstream server response with the status code ${statusCode} is not a valid JSON object: ` +
|
||||
@@ -503,12 +498,11 @@ export class JWProxy {
|
||||
pathname = pathname.replace('/wd/hub', '');
|
||||
}
|
||||
}
|
||||
return _.trimEnd(
|
||||
match && _.isArray(match.params?.command)
|
||||
? `/${match.params.command.join('/')}`
|
||||
: pathname,
|
||||
'/'
|
||||
);
|
||||
let result = pathname;
|
||||
if (match) {
|
||||
result = _.isArray(match.params?.command) ? `/${match.params.command.join('/')}` : '';
|
||||
}
|
||||
return _.trimEnd(result, '/');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
## jsonwp-status
|
||||
|
||||
Library of status codes for the Selenium [JSON Wire Protocol](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md).
|
||||
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
import { statusCodes } from 'appium-base-driver';
|
||||
|
||||
statusCodes.NoSuchContext;
|
||||
// -> {code: 35, summary: 'No such context found'}
|
||||
```
|
||||
|
||||
```
|
||||
import { getSummaryByCode } from 'appium-base-driver';
|
||||
|
||||
getSummaryByCode(0);
|
||||
// -> 'The command executed successfully.'
|
||||
```
|
||||
@@ -1,100 +0,0 @@
|
||||
## webdriver and mobile-json-wire protocols
|
||||
|
||||
An abstraction of the Mobile JSON Wire Protocol ([spec](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md)) and the W3C Wire Protocol ([spec](https://www.w3.org/TR/webdriver/) with Appium extensions (as specified [here](http://www.w3.org/TR/webdriver/#protocol-extensions)).
|
||||
|
||||
### Protocol Detection
|
||||
|
||||
In the event that a session is requested, and both MJSONWP _and_ W3C capabilities are provided, like this
|
||||
|
||||
```
|
||||
{
|
||||
"capabilities: {
|
||||
"alwaysMatch": {...},
|
||||
"firstMatch": [{...}, ...]
|
||||
},
|
||||
"desiredCapabilities": {...}
|
||||
}
|
||||
```
|
||||
|
||||
a W3C session will be served _unless_ the W3C Capabilities are incomplete. So if the `"desiredCapabilities"` object has more keys
|
||||
then whatever the capabilities were matched for the W3C capabilities, then an MJSONWP session will be served instead
|
||||
|
||||
|
||||
### Endpoints in the protocol
|
||||
|
||||
The Mobile JSON Wire Protocol package gives access to a number of endpoints documented [here](https://github.com/appium/appium-base-driver/blob/master/docs/mjsonwp/protocol-methods.md).
|
||||
|
||||
The W3C WebDriver Protocol package gives access to a number of endpoints documented in the [official documentation](https://www.w3.org/TR/webdriver/) and the
|
||||
[simplified spec](https://github.com/jlipps/simple-wd-spec)
|
||||
|
||||
### Protocol
|
||||
|
||||
The basic class, subclassed by drivers that will use the protocol.
|
||||
|
||||
|
||||
### routeConfiguringFunction (driver)
|
||||
|
||||
This function gives drivers access to the protocol routes. It returns a function that itself will take an [Express](http://expressjs.com/) application.
|
||||
|
||||
|
||||
### isSessionCommand (command)
|
||||
|
||||
Checks if the `command` needs to have a session associated with it.
|
||||
|
||||
|
||||
### ALL_COMMANDS
|
||||
|
||||
An array of all the commands that will be dispatched to by the Mobile JSON Wire Proxy endpoints.
|
||||
|
||||
|
||||
### NO_SESSION_ID_COMMANDS
|
||||
|
||||
An array of commands that do not need a session associated with them.
|
||||
|
||||
|
||||
### Errors
|
||||
|
||||
This package exports a number of classes and methods related to Selenium error handling. There are error classes for each Selenium error type (see [JSONWP Errors](https://code.google.com/p/selenium/wiki/JsonWireProtocol#Response_Status_Codes), as well as the context errors in the [mobile spec](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md#webviews-and-other-contexts).
|
||||
|
||||
The list of errors, and their meanings, can be found [here for JSONWP](https://github.com/appium/appium-base-driver/blob/master/docs/mjsonwp/errors.md) and
|
||||
[here for W3C Errors](https://www.w3.org/TR/webdriver/#handling-errors))
|
||||
|
||||
There are, in addition, two helper methods for dealing with errors
|
||||
|
||||
`isErrorType (err, type)`
|
||||
|
||||
- checks if the `err` object is a Mobile JSON Wire Protocol error of a particular type
|
||||
- arguments
|
||||
- `err` - the error object to test
|
||||
- `type` - the error class to test against
|
||||
- usage
|
||||
```js
|
||||
import { errors, isErrorType } from 'mobile-json-wire-protocol';
|
||||
|
||||
try {
|
||||
// do some stuff...
|
||||
} catch (err) {
|
||||
if (isErrorType(err, errors.InvalidCookieDomainError)) {
|
||||
// process...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`errorFromCode (code, message)`
|
||||
|
||||
- retrieve the appropriate error for an error code, with the supplied message.
|
||||
- arguments
|
||||
- `code` - the integer error code for a Mobile JSON Wire Protocol error
|
||||
- `message` - the message to be encapsulated in the error
|
||||
- usage
|
||||
```js
|
||||
import { errors, errorFromCode } from 'mobile-json-wire-protocol';
|
||||
|
||||
let error = errorFromCode(6, 'an error has occurred');
|
||||
|
||||
console.log(error instanceof errors.NoSuchDriverError);
|
||||
// => true
|
||||
|
||||
console.log(error.message === 'an error has occurred');
|
||||
// => true
|
||||
```
|
||||
@@ -12,7 +12,13 @@ import {
|
||||
validateExecuteMethodParams,
|
||||
} from './protocol';
|
||||
import {NO_SESSION_ID_COMMANDS, ALL_COMMANDS, METHOD_MAP, routeToCommandName} from './routes';
|
||||
import {errors, isErrorType, errorFromMJSONWPStatusCode, errorFromW3CJsonCode} from './errors';
|
||||
import {
|
||||
errors,
|
||||
isErrorType,
|
||||
errorFromMJSONWPStatusCode,
|
||||
errorFromW3CJsonCode,
|
||||
getResponseForW3CError,
|
||||
} from './errors';
|
||||
|
||||
export {
|
||||
routeConfiguringFunction,
|
||||
@@ -34,4 +40,5 @@ export {
|
||||
GET_STATUS_COMMAND,
|
||||
LIST_DRIVER_COMMANDS_COMMAND,
|
||||
LIST_DRIVER_EXTENSIONS_COMMAND,
|
||||
getResponseForW3CError,
|
||||
};
|
||||
|
||||
@@ -304,19 +304,23 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
// Appium specific
|
||||
//
|
||||
'/session/:sessionId/ime/available_engines': {
|
||||
GET: {command: 'availableIMEEngines'},
|
||||
GET: {command: 'availableIMEEngines', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/ime/active_engine': {
|
||||
GET: {command: 'getActiveIMEEngine'},
|
||||
GET: {command: 'getActiveIMEEngine', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/ime/activated': {
|
||||
GET: {command: 'isIMEActivated'},
|
||||
GET: {command: 'isIMEActivated', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/ime/deactivate': {
|
||||
POST: {command: 'deactivateIMEEngine'},
|
||||
POST: {command: 'deactivateIMEEngine', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/ime/activate': {
|
||||
POST: {command: 'activateIMEEngine', payloadParams: {required: ['engine']}},
|
||||
POST: {
|
||||
command: 'activateIMEEngine',
|
||||
payloadParams: {required: ['engine']},
|
||||
deprecated: true,
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/rotation': {
|
||||
GET: {command: 'getRotation'},
|
||||
@@ -341,16 +345,18 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
GET: {command: 'getContexts'},
|
||||
},
|
||||
'/session/:sessionId/network_connection': {
|
||||
GET: {command: 'getNetworkConnection'},
|
||||
GET: {command: 'getNetworkConnection', deprecated: true},
|
||||
POST: {
|
||||
command: 'setNetworkConnection',
|
||||
payloadParams: {unwrap: 'parameters', required: ['type']},
|
||||
deprecated: true,
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/receive_async_response': {
|
||||
POST: {
|
||||
command: 'receiveAsyncResponse',
|
||||
payloadParams: {required: ['status', 'value']},
|
||||
deprecated: true,
|
||||
},
|
||||
},
|
||||
'/appium/sessions': {
|
||||
|
||||
@@ -10,6 +10,9 @@ function msValidator(ms) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
const validators = {
|
||||
setUrl: (url) => {
|
||||
// either an `xyz://`, `about:`, or `data:` scheme is allowed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/base-driver",
|
||||
"version": "9.16.2",
|
||||
"version": "9.17.0",
|
||||
"description": "Base driver class for Appium drivers",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -44,12 +44,12 @@
|
||||
"test:types": "tsd"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^6.0.6",
|
||||
"@appium/types": "^0.25.1",
|
||||
"@appium/support": "^6.1.0",
|
||||
"@appium/types": "^0.25.3",
|
||||
"@colors/colors": "1.6.0",
|
||||
"async-lock": "1.4.1",
|
||||
"asyncbox": "3.0.0",
|
||||
"axios": "1.8.1",
|
||||
"axios": "1.8.4",
|
||||
"bluebird": "3.7.2",
|
||||
"body-parser": "1.20.3",
|
||||
"express": "4.21.2",
|
||||
@@ -62,7 +62,7 @@
|
||||
"path-to-regexp": "8.2.0",
|
||||
"serve-favicon": "2.5.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"type-fest": "4.36.0",
|
||||
"type-fest": "4.40.0",
|
||||
"validate.js": "0.13.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import {match} from 'path-to-regexp';
|
||||
import sinon from 'sinon';
|
||||
import log from '../../../lib/express/logger';
|
||||
import {handleLogContext} from '../../../lib/express/middleware';
|
||||
|
||||
describe('middleware', function () {
|
||||
before(async function () {
|
||||
@@ -21,4 +24,55 @@ describe('middleware', function () {
|
||||
match(pathname)(currentPathname).should.not.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleLogContext', function () {
|
||||
let req, res, next, updateAsyncContextStub;
|
||||
|
||||
beforeEach(function () {
|
||||
req = {
|
||||
headers: {},
|
||||
url: '/some/path'
|
||||
};
|
||||
res = {};
|
||||
next = sinon.spy();
|
||||
updateAsyncContextStub = sinon.stub(log, 'updateAsyncContext');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
updateAsyncContextStub.restore();
|
||||
});
|
||||
|
||||
it('should use provided x-request-id header', function () {
|
||||
const testRequestId = '123-test-id';
|
||||
req.headers['x-request-id'] = testRequestId;
|
||||
|
||||
handleLogContext(req, res, next);
|
||||
|
||||
updateAsyncContextStub.calledOnce.should.be.true;
|
||||
updateAsyncContextStub.firstCall.args[0].should.have.property('requestId', testRequestId);
|
||||
next.calledOnce.should.be.true;
|
||||
});
|
||||
|
||||
it('should handle x-request-id when provided as array', function () {
|
||||
const testRequestId = '123-test-id';
|
||||
req.headers['x-request-id'] = [testRequestId, 'ignored-id'];
|
||||
|
||||
handleLogContext(req, res, next);
|
||||
|
||||
updateAsyncContextStub.calledOnce.should.be.true;
|
||||
updateAsyncContextStub.firstCall.args[0].should.have.property('requestId', testRequestId);
|
||||
next.calledOnce.should.be.true;
|
||||
});
|
||||
|
||||
it('should generate uuid when x-request-id is not provided', function () {
|
||||
handleLogContext(req, res, next);
|
||||
|
||||
updateAsyncContextStub.calledOnce.should.be.true;
|
||||
updateAsyncContextStub.firstCall.args[0].should.have.property('requestId');
|
||||
updateAsyncContextStub.firstCall.args[0].requestId.should.match(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
);
|
||||
next.calledOnce.should.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,9 +7,6 @@ function buildReqRes(url, method, body) {
|
||||
let req = {originalUrl: url, method, body};
|
||||
let res = {};
|
||||
res.headers = {};
|
||||
res.set = (k, v) => {
|
||||
res[k] = v;
|
||||
};
|
||||
res.setHeader = (k, v) => {
|
||||
res.headers[k] = v;
|
||||
};
|
||||
@@ -110,6 +107,11 @@ describe('proxy', function () {
|
||||
`http://${TEST_HOST}:${port}/session/123/element/200/value`
|
||||
);
|
||||
|
||||
mockProxy({sessionId: '123', reqBasePath: '/wd/hub'})
|
||||
.getUrlForProxy('/wd/hub/session/456', 'GET').should.eql(
|
||||
`http://${TEST_HOST}:${port}/session/123`
|
||||
);
|
||||
|
||||
mockProxy({reqBasePath: '/my/base/path'})
|
||||
.getUrlForProxy('/my/base/path/session', 'POST').should.eql(
|
||||
`http://${TEST_HOST}:${port}/session`
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.3.6](https://github.com/appium/appium/compare/@appium/base-plugin@2.3.5...@appium/base-plugin@2.3.6) (2025-04-25)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.5](https://github.com/appium/appium/compare/@appium/base-plugin@2.3.4...@appium/base-plugin@2.3.5) (2025-03-17)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.4](https://github.com/appium/appium/compare/@appium/base-plugin@2.3.3...@appium/base-plugin@2.3.4) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.3](https://github.com/appium/appium/compare/@appium/base-plugin@2.3.2...@appium/base-plugin@2.3.3) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/base-plugin",
|
||||
"version": "2.3.3",
|
||||
"version": "2.3.6",
|
||||
"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.16.2",
|
||||
"@appium/support": "^6.0.6"
|
||||
"@appium/base-driver": "^9.17.0",
|
||||
"@appium/support": "^6.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.1.0](https://github.com/appium/appium/compare/@appium/docutils@1.0.34...@appium/docutils@1.1.0) (2025-04-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **docutils:** add MkDocs Material codeblock & text format features ([#21184](https://github.com/appium/appium/issues/21184)) ([55da22f](https://github.com/appium/appium/commit/55da22fc4000a1142782cc5fddcb526cb30b2f09))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **docutils:** add & adjust various log messages ([#21160](https://github.com/appium/appium/issues/21160)) ([804b1a3](https://github.com/appium/appium/commit/804b1a3c15acdf95066d45e7c36518cb8600dbbb))
|
||||
|
||||
|
||||
|
||||
## [1.0.34](https://github.com/appium/appium/compare/@appium/docutils@1.0.33...@appium/docutils@1.0.34) (2025-03-17)
|
||||
|
||||
**Note:** Version bump only for package @appium/docutils
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.33](https://github.com/appium/appium/compare/@appium/docutils@1.0.32...@appium/docutils@1.0.33) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/docutils
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.32](https://github.com/appium/appium/compare/@appium/docutils@1.0.31...@appium/docutils@1.0.32) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/docutils
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
markdown_extensions:
|
||||
- meta
|
||||
- attr_list
|
||||
- admonition
|
||||
- attr_list
|
||||
- footnotes
|
||||
- md_in_html
|
||||
- pymdownx.highlight:
|
||||
anchor_linenums: true
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.snippets
|
||||
- pymdownx.superfences
|
||||
- meta
|
||||
- pymdownx.caret
|
||||
- pymdownx.critic
|
||||
- pymdownx.details
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
- footnotes
|
||||
- pymdownx.highlight:
|
||||
anchor_linenums: true
|
||||
line_spans: __span
|
||||
pygments_lang_class: true
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.keys
|
||||
- pymdownx.mark
|
||||
- pymdownx.snippets
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
- pymdownx.tilde
|
||||
- tables
|
||||
- toc:
|
||||
permalink: true
|
||||
|
||||
@@ -63,6 +73,9 @@ theme:
|
||||
quote: octicons/quote-16
|
||||
features:
|
||||
- content.action.edit
|
||||
- content.code.annotate
|
||||
- content.code.copy
|
||||
- content.tabs.link
|
||||
- navigation.sections
|
||||
- navigation.tabs
|
||||
- navigation.top
|
||||
|
||||
@@ -16,10 +16,9 @@ import {
|
||||
NAME_BIN,
|
||||
NAME_MIKE,
|
||||
NAME_MKDOCS_YML,
|
||||
NAME_PYTHON,
|
||||
} from '../constants';
|
||||
import {DocutilsError} from '../error';
|
||||
import {findMike, findMkDocsYml, findPython, readPackageJson} from '../fs';
|
||||
import {findMike, findMkDocsYml, isMkDocsInstalled, readPackageJson, requirePython} from '../fs';
|
||||
import {getLogger} from '../logger';
|
||||
import {argify, spawnBackgroundProcess, SpawnBackgroundProcessOpts, stopwatch} from '../util';
|
||||
|
||||
@@ -37,6 +36,7 @@ async function doServe(
|
||||
opts: SpawnBackgroundProcessOpts = {},
|
||||
) {
|
||||
const finalArgs = ['serve', ...args];
|
||||
log.debug('Executing %s via: %s %O', NAME_MIKE, mikePath, finalArgs);
|
||||
return spawnBackgroundProcess(mikePath, finalArgs, opts);
|
||||
}
|
||||
|
||||
@@ -99,18 +99,17 @@ export async function deploy({
|
||||
}: DeployOpts = {}) {
|
||||
const stop = stopwatch('deploy');
|
||||
|
||||
const pythonPath = await findPython();
|
||||
await requirePython();
|
||||
|
||||
if (!pythonPath) {
|
||||
throw new DocutilsError(
|
||||
`Could not find ${NAME_PYTHON}3/${NAME_PYTHON} executable in PATH; please install Python v3`,
|
||||
);
|
||||
const mkdocsInstalled = await isMkDocsInstalled();
|
||||
if (!mkdocsInstalled) {
|
||||
throw new DocutilsError(`Could not find MkDocs executable; please run "${NAME_BIN} init"`);
|
||||
}
|
||||
|
||||
mkDocsYmlPath = mkDocsYmlPath ?? (await findMkDocsYml(cwd));
|
||||
if (!mkDocsYmlPath) {
|
||||
throw new DocutilsError(
|
||||
`Could not find ${NAME_MKDOCS_YML} from ${cwd}; run "${NAME_BIN} init" to create it`,
|
||||
`Could not find ${NAME_MKDOCS_YML} from ${cwd}; please run "${NAME_BIN} init"`,
|
||||
);
|
||||
}
|
||||
version = version ?? (await findDeployVersion(packageJsonPath, cwd));
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
|
||||
import path from 'node:path';
|
||||
import {exec, TeenProcessExecOptions} from 'teen_process';
|
||||
import {DEFAULT_SITE_DIR, NAME_BIN, NAME_MKDOCS, NAME_MKDOCS_YML, NAME_PYTHON} from '../constants';
|
||||
import {DEFAULT_SITE_DIR, NAME_BIN, NAME_MKDOCS, NAME_MKDOCS_YML} from '../constants';
|
||||
import {DocutilsError} from '../error';
|
||||
import {findMkDocsYml, findPython, readMkDocsYml} from '../fs';
|
||||
import {findMkDocsYml, isMkDocsInstalled, readMkDocsYml, requirePython} from '../fs';
|
||||
import {getLogger} from '../logger';
|
||||
import {relative, spawnBackgroundProcess, SpawnBackgroundProcessOpts, stopwatch} from '../util';
|
||||
|
||||
@@ -58,11 +58,11 @@ export async function buildSite({
|
||||
}: BuildMkDocsOpts = {}) {
|
||||
const stop = stopwatch('build-mkdocs');
|
||||
|
||||
const pythonPath = await findPython();
|
||||
if (!pythonPath) {
|
||||
throw new DocutilsError(
|
||||
`Could not find ${NAME_PYTHON}3/${NAME_PYTHON} executable in PATH; please install Python v3`,
|
||||
);
|
||||
const pythonPath = await requirePython();
|
||||
|
||||
const mkdocsInstalled = await isMkDocsInstalled();
|
||||
if (!mkdocsInstalled) {
|
||||
throw new DocutilsError(`Could not find MkDocs executable; please run "${NAME_BIN} init"`);
|
||||
}
|
||||
|
||||
mkDocsYmlPath = mkDocsYmlPath
|
||||
@@ -70,7 +70,7 @@ export async function buildSite({
|
||||
: await findMkDocsYml(cwd);
|
||||
if (!mkDocsYmlPath) {
|
||||
throw new DocutilsError(
|
||||
`Could not find ${NAME_MKDOCS_YML} from ${cwd}; run "${NAME_BIN} init" to create it`,
|
||||
`Could not find ${NAME_MKDOCS_YML} from ${cwd}; please run "${NAME_BIN} init"`,
|
||||
);
|
||||
}
|
||||
const mkdocsArgs = ['-f', mkDocsYmlPath];
|
||||
|
||||
@@ -73,11 +73,6 @@ export const NAME_TYPESCRIPT = 'typescript';
|
||||
*/
|
||||
export const NAME_ERR_ENOENT = 'ENOENT';
|
||||
|
||||
/**
|
||||
* Code for a "file already exists" error
|
||||
*/
|
||||
export const NAME_ERR_EEXIST = 'EEXIST';
|
||||
|
||||
/**
|
||||
* Name of the default theme
|
||||
*/
|
||||
@@ -153,3 +148,8 @@ export const DEFAULT_SITE_DIR = 'site';
|
||||
* To ensure backwards compatibility, its environment variable version is used
|
||||
*/
|
||||
export const PIP_ENV_VARS = {PIP_BREAK_SYSTEM_PACKAGES: '1'};
|
||||
|
||||
/**
|
||||
* Error message emitted when the path to Python was not found
|
||||
*/
|
||||
export const MESSAGE_PYTHON_MISSING = 'Could not find Python in PATH. Is it installed?';
|
||||
|
||||
@@ -11,7 +11,15 @@ import _pkgDir from 'pkg-dir';
|
||||
import readPkg, {NormalizedPackageJson, PackageJson} from 'read-pkg';
|
||||
import {JsonValue} from 'type-fest';
|
||||
import YAML from 'yaml';
|
||||
import {NAME_MIKE, NAME_MKDOCS_YML, NAME_NPM, NAME_PACKAGE_JSON, NAME_PYTHON} from './constants';
|
||||
import {
|
||||
MESSAGE_PYTHON_MISSING,
|
||||
NAME_MIKE,
|
||||
NAME_MKDOCS,
|
||||
NAME_MKDOCS_YML,
|
||||
NAME_NPM,
|
||||
NAME_PACKAGE_JSON,
|
||||
NAME_PYTHON,
|
||||
} from './constants';
|
||||
import {DocutilsError} from './error';
|
||||
import {getLogger} from './logger';
|
||||
import {MkDocsYml} from './model';
|
||||
@@ -152,18 +160,14 @@ export const readJson = _.memoize(
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes a file, but will not overwrite an existing file unless `overwrite` is true
|
||||
*
|
||||
* Will stringify JSON objects
|
||||
* Writes contents to a file. Any JSON objects are stringified
|
||||
* @param filepath - Path to file
|
||||
* @param content - File contents
|
||||
* @param overwrite - If `true`, overwrite existing files
|
||||
*/
|
||||
export function safeWriteFile(filepath: string, content: JsonValue, overwrite = false) {
|
||||
export function writeFileString(filepath: string, content: JsonValue) {
|
||||
const data: string = _.isString(content) ? content : JSON.stringify(content, undefined, 2);
|
||||
return fs.writeFile(filepath, data, {
|
||||
encoding: 'utf8',
|
||||
flag: overwrite ? 'w' : 'wx',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -189,6 +193,28 @@ const whichPython = _.partial(cachedWhich, NAME_PYTHON, {nothrow: true});
|
||||
*/
|
||||
const whichPython3 = _.partial(cachedWhich, `${NAME_PYTHON}3`, {nothrow: true});
|
||||
|
||||
/**
|
||||
* Check if `mkdocs` is installed
|
||||
*/
|
||||
export const isMkDocsInstalled = _.memoize(async (): Promise<boolean> => {
|
||||
// see if it's in PATH
|
||||
const mkDocsPath = await cachedWhich(NAME_MKDOCS, {nothrow: true});
|
||||
if (mkDocsPath) {
|
||||
return true;
|
||||
}
|
||||
// if it isn't, it should be invokable via `python -m`
|
||||
const pythonPath = await findPython();
|
||||
if (!pythonPath) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await exec(pythonPath, ['-m', NAME_MKDOCS]);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* `mike` cannot be invoked via `python -m`, so we need to find the script.
|
||||
*/
|
||||
@@ -225,6 +251,17 @@ export const findPython = _.memoize(
|
||||
async (): Promise<string | null> => (await whichPython3()) ?? (await whichPython()),
|
||||
);
|
||||
|
||||
/**
|
||||
* Check if a path to Python exists, otherwise raise DocutilsError
|
||||
*/
|
||||
export async function requirePython(pythonPath?: string): Promise<string> {
|
||||
const foundPythonPath = pythonPath ?? (await findPython());
|
||||
if (!foundPythonPath) {
|
||||
throw new DocutilsError(MESSAGE_PYTHON_MISSING);
|
||||
}
|
||||
return foundPythonPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an `mkdocs.yml` file, merges inherited configs, and returns the result. The result is cached.
|
||||
*
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import * as JSON5 from 'json5';
|
||||
import {NAME_MKDOCS_YML, NAME_TSCONFIG_JSON, NAME_PYTHON, PIP_ENV_VARS, REQUIREMENTS_TXT_PATH} from './constants';
|
||||
import {NAME_MKDOCS_YML, NAME_TSCONFIG_JSON, PIP_ENV_VARS, REQUIREMENTS_TXT_PATH} from './constants';
|
||||
import YAML from 'yaml';
|
||||
import {exec} from 'teen_process';
|
||||
import {Simplify} from 'type-fest';
|
||||
@@ -15,7 +15,7 @@ import type { ScaffoldTask } from './scaffold';
|
||||
import {getLogger} from './logger';
|
||||
import {MkDocsYml, TsConfigJson} from './model';
|
||||
import _ from 'lodash';
|
||||
import {findPython, stringifyJson5, stringifyYaml} from './fs';
|
||||
import {requirePython, stringifyJson5, stringifyYaml} from './fs';
|
||||
|
||||
/**
|
||||
* Data for the base `mkdocs.yml` file
|
||||
@@ -139,7 +139,7 @@ export async function initPython({
|
||||
dryRun = false,
|
||||
upgrade = false,
|
||||
}: InitPythonOptions = {}): Promise<void> {
|
||||
pythonPath = pythonPath ?? (await findPython()) ?? NAME_PYTHON;
|
||||
const foundPythonPath = await requirePython(pythonPath);
|
||||
|
||||
const args = ['-m', 'pip', 'install', '-r', REQUIREMENTS_TXT_PATH];
|
||||
if (upgrade) {
|
||||
@@ -148,19 +148,19 @@ export async function initPython({
|
||||
if (dryRun) {
|
||||
dryRunLog.info(
|
||||
'Would execute command: %s %s (environment variables: %s)',
|
||||
pythonPath,
|
||||
foundPythonPath,
|
||||
args.join(' '),
|
||||
PIP_ENV_VARS,
|
||||
);
|
||||
} else {
|
||||
log.debug('Executing command: %s %s (environment variables: %s)',
|
||||
pythonPath,
|
||||
foundPythonPath,
|
||||
args.join(' '),
|
||||
PIP_ENV_VARS,
|
||||
);
|
||||
log.info('Installing Python dependencies...');
|
||||
try {
|
||||
const result = await exec(pythonPath, args, {env: PIP_ENV_VARS, shell: true});
|
||||
const result = await exec(foundPythonPath, args, {env: PIP_ENV_VARS, shell: true});
|
||||
const {code, stdout} = result;
|
||||
if (code !== 0) {
|
||||
throw new DocutilsError(`Could not install Python dependencies. Reason: ${stdout}`);
|
||||
@@ -171,7 +171,7 @@ export async function initPython({
|
||||
);
|
||||
}
|
||||
}
|
||||
log.success('Installed Python dependencies (or dependencies already installed)');
|
||||
log.success('Successfully installed Python dependencies');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,8 +12,8 @@ import {JsonValue, JsonObject} from 'type-fest';
|
||||
import {DocutilsError} from './error';
|
||||
import {relative} from './util';
|
||||
import _ from 'lodash';
|
||||
import {stringifyJson, readPackageJson, safeWriteFile} from './fs';
|
||||
import {NAME_ERR_ENOENT, NAME_ERR_EEXIST} from './constants';
|
||||
import {stringifyJson, readPackageJson, writeFileString} from './fs';
|
||||
import {NAME_ERR_ENOENT} from './constants';
|
||||
|
||||
const log = getLogger('init');
|
||||
const dryRunLog = getLogger('dry-run', log);
|
||||
@@ -91,7 +91,7 @@ export function createScaffoldTask<Opts extends ScaffoldTaskOptions, T extends J
|
||||
dest = dest ?? path.join(pkgDir, defaultFilename);
|
||||
const relativeDest = relativePath(dest);
|
||||
log.debug('Initializing %s', relativeDest);
|
||||
let shouldWriteDest = false;
|
||||
let destChangesNeeded = false;
|
||||
let isNew = false;
|
||||
let destContent: T;
|
||||
let result: ScaffoldTaskResult<T>;
|
||||
@@ -103,7 +103,7 @@ export function createScaffoldTask<Opts extends ScaffoldTaskOptions, T extends J
|
||||
if (err.code !== NAME_ERR_ENOENT) {
|
||||
throw err;
|
||||
}
|
||||
shouldWriteDest = true;
|
||||
destChangesNeeded = true;
|
||||
log.debug('Creating new file %s', relativeDest);
|
||||
destContent = {} as T;
|
||||
isNew = true;
|
||||
@@ -112,9 +112,9 @@ export function createScaffoldTask<Opts extends ScaffoldTaskOptions, T extends J
|
||||
const defaults: T = transform(defaultContent, opts, pkg);
|
||||
const finalDestContent: T = _.defaultsDeep({}, destContent, defaults);
|
||||
|
||||
shouldWriteDest = shouldWriteDest || !_.isEqual(destContent, finalDestContent);
|
||||
destChangesNeeded = destChangesNeeded || !_.isEqual(destContent, finalDestContent);
|
||||
|
||||
if (shouldWriteDest) {
|
||||
if (destChangesNeeded) {
|
||||
log.info('Changes needed in %s', relativeDest);
|
||||
log.debug('Original %s: %O', relativeDest, destContent);
|
||||
log.debug('Final %s: %O', relativeDest, finalDestContent);
|
||||
@@ -126,20 +126,19 @@ export function createScaffoldTask<Opts extends ScaffoldTaskOptions, T extends J
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
await safeWriteFile(dest, finalDestContent, overwrite);
|
||||
if (isNew) {
|
||||
log.success('Initialized %s', description);
|
||||
} else {
|
||||
log.success('Updated %s', description);
|
||||
}
|
||||
} catch (e) {
|
||||
const err = e as NodeJS.ErrnoException;
|
||||
// this should only be thrown if `force` is false
|
||||
if (err.code === NAME_ERR_EEXIST) {
|
||||
log.info(`${relativeDest} already exists; continuing...`);
|
||||
log.debug(`Tried to apply patch:\n\n${patch}`);
|
||||
} else {
|
||||
if (!isNew && !overwrite) {
|
||||
log.info('File %s already exists, continuing (enable overwrite with "--force")', relativeDest);
|
||||
log.debug('Tried to apply patch:\n\n%s', patch);
|
||||
} else {
|
||||
try {
|
||||
await writeFileString(dest, finalDestContent);
|
||||
if (isNew) {
|
||||
log.success('Initialized %s', description);
|
||||
} else {
|
||||
log.success('Updated %s', description);
|
||||
}
|
||||
} catch (e) {
|
||||
const err = e as NodeJS.ErrnoException;
|
||||
throw new DocutilsError(`Could not write to ${relativeDest}. Reason: ${err.message}`, {
|
||||
cause: err,
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {fs, util} from '@appium/support';
|
||||
import {fs, npm, util} from '@appium/support';
|
||||
import chalk from 'chalk';
|
||||
import _ from 'lodash';
|
||||
import {EventEmitter} from 'node:events';
|
||||
@@ -13,6 +13,7 @@ import {satisfies} from 'semver';
|
||||
import {exec} from 'teen_process';
|
||||
import {
|
||||
DOCUTILS_PKG,
|
||||
MESSAGE_PYTHON_MISSING,
|
||||
NAME_BIN,
|
||||
NAME_ERR_ENOENT,
|
||||
NAME_MKDOCS,
|
||||
@@ -27,7 +28,15 @@ import {
|
||||
REQUIREMENTS_TXT_PATH,
|
||||
} from './constants';
|
||||
import {DocutilsError} from './error';
|
||||
import {findMkDocsYml, findPkgDir, readJson5, readMkDocsYml, whichNpm, findPython} from './fs';
|
||||
import {
|
||||
findMkDocsYml,
|
||||
findPkgDir,
|
||||
isMkDocsInstalled,
|
||||
readJson5,
|
||||
readMkDocsYml,
|
||||
whichNpm,
|
||||
findPython,
|
||||
} from './fs';
|
||||
import {getLogger} from './logger';
|
||||
import {MkDocsYml, PipPackage} from './model';
|
||||
import {relative} from './util';
|
||||
@@ -281,12 +290,16 @@ export class DocutilsValidator extends EventEmitter {
|
||||
* Validates that the correct version of `mkdocs` is installed
|
||||
*/
|
||||
protected async validateMkDocs() {
|
||||
const pythonPath = this.pythonPath ?? (await findPython());
|
||||
log.debug(`Validating MkDocs version`);
|
||||
|
||||
const pythonPath = this.pythonPath ?? (await findPython());
|
||||
if (!pythonPath) {
|
||||
return this.fail(
|
||||
`Could not find ${NAME_PYTHON} executable in PATH. If it is installed, check your PATH environment variable.`,
|
||||
);
|
||||
return this.fail(MESSAGE_PYTHON_MISSING);
|
||||
}
|
||||
|
||||
const mkdocsInstalled = await isMkDocsInstalled();
|
||||
if (!mkdocsInstalled) {
|
||||
return this.fail(`Could not find MkDocs executable; please run "${NAME_BIN} init"`);
|
||||
}
|
||||
|
||||
let rawMkDocsVersion: string | undefined;
|
||||
@@ -296,26 +309,21 @@ export class DocutilsValidator extends EventEmitter {
|
||||
return this.fail(`Failed to get MkDocs version: ${err}`);
|
||||
}
|
||||
const match = rawMkDocsVersion.match(MKDOCS_VERSION_REGEX);
|
||||
if (match) {
|
||||
const version = match[1];
|
||||
const reqs = await this.parseRequirementsTxt();
|
||||
const mkDocsPipPkg = _.find(reqs, {name: NAME_MKDOCS});
|
||||
if (!mkDocsPipPkg) {
|
||||
throw new DocutilsError(
|
||||
`No ${NAME_MKDOCS} package in ${REQUIREMENTS_TXT_PATH}. This is a bug.`,
|
||||
);
|
||||
}
|
||||
const {version: mkDocsReqdVersion} = mkDocsPipPkg;
|
||||
if (version !== mkDocsReqdVersion) {
|
||||
return this.fail(
|
||||
`${NAME_MKDOCS} is v${version}, but ${REQUIREMENTS_TXT_PATH} requires v${mkDocsReqdVersion}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (!match) {
|
||||
return this.fail(`Could not parse MkDocs version. Output was ${rawMkDocsVersion}`);
|
||||
}
|
||||
const version = match[1];
|
||||
const reqs = await this.parseRequirementsTxt();
|
||||
const mkDocsPipPkg = _.find(reqs, {name: NAME_MKDOCS});
|
||||
if (!mkDocsPipPkg) {
|
||||
throw new DocutilsError(
|
||||
`Could not parse version from MkDocs. This is a bug. Output was ${rawMkDocsVersion}`,
|
||||
`No ${NAME_MKDOCS} package in ${REQUIREMENTS_TXT_PATH}. This is a bug`,
|
||||
);
|
||||
}
|
||||
const {version: mkDocsReqdVersion} = mkDocsPipPkg;
|
||||
if (version !== mkDocsReqdVersion) {
|
||||
return this.fail(`MkDocs v${version} is installed, but v${mkDocsReqdVersion} is required`);
|
||||
}
|
||||
|
||||
this.ok('MkDocs install OK');
|
||||
}
|
||||
@@ -325,13 +333,16 @@ export class DocutilsValidator extends EventEmitter {
|
||||
*
|
||||
* It checks if the file exists, if it can be parsed as YAML, and if it has a `site_name` property.
|
||||
*/
|
||||
protected async validateMkDocsConfig(mkDocsYmlPath?: string) {
|
||||
mkDocsYmlPath = mkDocsYmlPath ?? this.mkDocsYmlPath ?? (await findMkDocsYml(this.cwd));
|
||||
protected async validateMkDocsConfig() {
|
||||
log.debug(`Validating ${NAME_MKDOCS_YML}`);
|
||||
|
||||
const mkDocsYmlPath = this.mkDocsYmlPath ?? (await findMkDocsYml(this.cwd));
|
||||
if (!mkDocsYmlPath) {
|
||||
return this.fail(
|
||||
`Could not find ${NAME_MKDOCS_YML} from ${this.cwd}. Run "${NAME_BIN} init" to create it`,
|
||||
`Could not find ${NAME_MKDOCS_YML} from ${this.cwd}; please run "${NAME_BIN} init"`,
|
||||
);
|
||||
}
|
||||
|
||||
let mkDocsYml: MkDocsYml;
|
||||
try {
|
||||
mkDocsYml = await readMkDocsYml(mkDocsYmlPath);
|
||||
@@ -358,24 +369,25 @@ export class DocutilsValidator extends EventEmitter {
|
||||
* This is required because other validators need `npm exec` to work, which is only available in npm 7+.
|
||||
*/
|
||||
protected async validateNpmVersion() {
|
||||
log.debug(`Validating ${NAME_NPM} version`);
|
||||
|
||||
const npmEngineRange = DOCUTILS_PKG.engines?.npm;
|
||||
if (!npmEngineRange) {
|
||||
throw new DocutilsError('Could not find property engines.npm in package.json. This is a bug');
|
||||
throw new DocutilsError(`Could not find property 'engines.npm' in ${NAME_PACKAGE_JSON}. This is a bug`);
|
||||
}
|
||||
|
||||
const npmPath = this.npmPath ?? (await whichNpm());
|
||||
if (!npmPath) {
|
||||
throw new DocutilsError(`Could not find ${NAME_NPM} in PATH. That seems weird, doesn't it?`);
|
||||
}
|
||||
|
||||
try {
|
||||
const npmPath = this.npmPath ?? (await whichNpm());
|
||||
if (!npmPath) {
|
||||
throw new DocutilsError(
|
||||
`Could not find ${NAME_NPM} in PATH. That seems weird, doesn't it?`,
|
||||
);
|
||||
}
|
||||
const {stdout: npmVersion} = await exec(npmPath, ['-v']);
|
||||
const {stdout: npmVersion} = await npm.exec('-v', [], {cwd: this.cwd});
|
||||
if (!satisfies(npmVersion.trim(), npmEngineRange)) {
|
||||
this.fail(`${NAME_NPM} is version ${npmVersion}, but ${npmEngineRange} is required`);
|
||||
return;
|
||||
return this.fail(`${NAME_NPM} v${npmVersion} is installed, but ${npmEngineRange} is required`);
|
||||
}
|
||||
} catch {
|
||||
return this.fail(`Could not find ${this.npmPath} in PATH. Is it installed?`);
|
||||
return this.fail(`Could not retrieve ${NAME_NPM} version`);
|
||||
}
|
||||
this.ok(`${NAME_NPM} version OK`);
|
||||
}
|
||||
@@ -387,11 +399,14 @@ export class DocutilsValidator extends EventEmitter {
|
||||
* contents of our `requirements.txt`. Versions _must_ match exactly.
|
||||
*/
|
||||
protected async validatePythonDeps() {
|
||||
let pipListOutput: string;
|
||||
log.debug(`Validating required ${NAME_PYTHON} package versions`);
|
||||
|
||||
const pythonPath = this.pythonPath ?? (await findPython());
|
||||
if (!pythonPath) {
|
||||
return this.fail(`Could not find ${NAME_PYTHON} in PATH. Is it installed?`);
|
||||
return this.fail(MESSAGE_PYTHON_MISSING);
|
||||
}
|
||||
|
||||
let pipListOutput: string;
|
||||
try {
|
||||
({stdout: pipListOutput} = await exec(pythonPath, [
|
||||
'-m',
|
||||
@@ -401,16 +416,16 @@ export class DocutilsValidator extends EventEmitter {
|
||||
'json',
|
||||
]));
|
||||
} catch {
|
||||
return this.fail(`Could not find ${NAME_PIP} in PATH. Is it installed?`);
|
||||
return this.fail(
|
||||
`Could not find ${NAME_PIP} installation for Python at ${pythonPath}. Is it installed?`
|
||||
);
|
||||
}
|
||||
|
||||
let installedPkgs: PipPackage[];
|
||||
try {
|
||||
installedPkgs = JSON.parse(pipListOutput) as PipPackage[];
|
||||
} catch {
|
||||
throw new DocutilsError(
|
||||
`Could not parse output of "${NAME_PIP} list" as JSON: ${pipListOutput}`,
|
||||
);
|
||||
return this.fail(`Could not parse output of "${NAME_PIP} list" as JSON: ${pipListOutput}`);
|
||||
}
|
||||
|
||||
const pkgsByName = _.mapValues(_.keyBy(installedPkgs, 'name'), 'version');
|
||||
@@ -423,8 +438,7 @@ export class DocutilsValidator extends EventEmitter {
|
||||
const version = pkgsByName[reqdPkg.name];
|
||||
if (!version) {
|
||||
missingPackages.push(reqdPkg);
|
||||
}
|
||||
if (version !== reqdPkg.version) {
|
||||
} else if (version !== reqdPkg.version) {
|
||||
invalidVersionPackages.push([reqdPkg, {name: reqdPkg.name, version}]);
|
||||
}
|
||||
}
|
||||
@@ -464,36 +478,44 @@ export class DocutilsValidator extends EventEmitter {
|
||||
* Asserts that the Python version is 3.x
|
||||
*/
|
||||
protected async validatePythonVersion() {
|
||||
log.debug(`Validating ${NAME_PYTHON} version`);
|
||||
|
||||
const pythonPath = this.pythonPath ?? (await findPython());
|
||||
if (!pythonPath) {
|
||||
return this.fail(`Could not find ${NAME_PYTHON} in PATH. Is it installed?`);
|
||||
return this.fail(MESSAGE_PYTHON_MISSING);
|
||||
}
|
||||
|
||||
try {
|
||||
const {stdout} = await exec(pythonPath, ['--version']);
|
||||
if (!stdout.includes(PYTHON_VER_STR)) {
|
||||
return this.fail(
|
||||
`Could not find Python 3.x in PATH; found ${stdout}. Please use --python-path`,
|
||||
);
|
||||
return this.fail(`Could not find Python 3.x in PATH; found ${stdout}`);
|
||||
}
|
||||
} catch {
|
||||
return this.fail(`Could not find Python 3.x in PATH.`);
|
||||
return this.fail(`Could not retrieve Python version`);
|
||||
}
|
||||
this.ok('Python version OK');
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that TypeScript is installed, runnable, the correct version, and a parseable `tsconfig.json` exists.
|
||||
* Asserts that TypeScript is installed, runnable, and the correct version.
|
||||
*/
|
||||
protected async validateTypeScript() {
|
||||
log.debug(`Validating ${NAME_TYPESCRIPT} version`);
|
||||
|
||||
const pkgDir = await this.findPkgDir();
|
||||
if (!pkgDir) {
|
||||
return this.fail(`Could not find package.json in ${this.cwd}`);
|
||||
return this.fail(`Could not find ${NAME_PACKAGE_JSON} in ${this.cwd}`);
|
||||
}
|
||||
|
||||
const npmPath = this.npmPath ?? (await whichNpm());
|
||||
if (!npmPath) {
|
||||
throw new DocutilsError(`Could not find ${NAME_NPM} in PATH. That seems weird, doesn't it?`);
|
||||
}
|
||||
|
||||
let typeScriptVersion: string;
|
||||
let rawTypeScriptVersion: string;
|
||||
try {
|
||||
({stdout: rawTypeScriptVersion} = await exec(NAME_NPM, ['exec', 'tsc', '--', '--version'], {
|
||||
({stdout: rawTypeScriptVersion} = await npm.exec('exec', ['tsc', '--', '--version'], {
|
||||
cwd: pkgDir,
|
||||
}));
|
||||
} catch {
|
||||
@@ -510,16 +532,15 @@ export class DocutilsValidator extends EventEmitter {
|
||||
}
|
||||
|
||||
const reqdTypeScriptVersion = DOCUTILS_PKG.dependencies?.typescript;
|
||||
|
||||
if (!reqdTypeScriptVersion) {
|
||||
throw new DocutilsError(
|
||||
`Could not find a dep for ${NAME_TYPESCRIPT} in ${NAME_PACKAGE_JSON}. This is a bug.`,
|
||||
`Could not find ${NAME_TYPESCRIPT} dependency in ${NAME_PACKAGE_JSON}. This is a bug`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!satisfies(typeScriptVersion, reqdTypeScriptVersion)) {
|
||||
return this.fail(
|
||||
`Found TypeScript version ${typeScriptVersion}, but ${reqdTypeScriptVersion} is required`,
|
||||
`TypeScript v${typeScriptVersion} is installed, but v${reqdTypeScriptVersion} is required`,
|
||||
);
|
||||
}
|
||||
this.ok('TypeScript install OK');
|
||||
@@ -529,25 +550,23 @@ export class DocutilsValidator extends EventEmitter {
|
||||
* Validates a `tsconfig.json` file
|
||||
*/
|
||||
protected async validateTypeScriptConfig() {
|
||||
log.debug(`Validating ${NAME_TSCONFIG_JSON}`);
|
||||
|
||||
const pkgDir = await this.findPkgDir();
|
||||
if (!pkgDir) {
|
||||
return this.fail(new DocutilsError(`Could not find package.json in ${this.cwd}`));
|
||||
return this.fail(`Could not find ${NAME_PACKAGE_JSON} in ${this.cwd}`);
|
||||
}
|
||||
const tsconfigJsonPath = (this.tsconfigJsonPath =
|
||||
this.tsconfigJsonPath ?? path.join(pkgDir, NAME_TSCONFIG_JSON));
|
||||
|
||||
const tsconfigJsonPath = this.tsconfigJsonPath ?? path.join(pkgDir, NAME_TSCONFIG_JSON);
|
||||
const relTsconfigJsonPath = relative(this.cwd, tsconfigJsonPath);
|
||||
try {
|
||||
await readJson5(tsconfigJsonPath);
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
return this.fail(
|
||||
new DocutilsError(`Unparseable ${NAME_TSCONFIG_JSON} at ${relTsconfigJsonPath}: ${e}`),
|
||||
);
|
||||
return this.fail(`Could not parse ${NAME_TSCONFIG_JSON} at ${relTsconfigJsonPath}: ${e}`);
|
||||
}
|
||||
return this.fail(
|
||||
new DocutilsError(
|
||||
`Missing ${NAME_TSCONFIG_JSON} at ${relTsconfigJsonPath}; "${NAME_BIN} init" can help`,
|
||||
),
|
||||
`Could not find ${NAME_TSCONFIG_JSON} at ${relTsconfigJsonPath}; please run "${NAME_BIN} init"`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/docutils",
|
||||
"version": "1.0.32",
|
||||
"version": "1.1.0",
|
||||
"description": "Documentation generation utilities for Appium and related projects",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -48,11 +48,11 @@
|
||||
"start": "node ./build/lib/cli/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^6.0.6",
|
||||
"@appium/tsconfig": "^0.3.4",
|
||||
"@appium/support": "^6.1.0",
|
||||
"@appium/tsconfig": "^0.3.5",
|
||||
"@sliphua/lilconfig-ts-loader": "3.2.2",
|
||||
"chalk": "4.1.2",
|
||||
"consola": "3.4.0",
|
||||
"consola": "3.4.2",
|
||||
"diff": "7.0.0",
|
||||
"json5": "2.2.3",
|
||||
"lilconfig": "3.1.3",
|
||||
@@ -62,9 +62,9 @@
|
||||
"semver": "7.7.1",
|
||||
"source-map-support": "0.5.21",
|
||||
"teen_process": "2.3.1",
|
||||
"type-fest": "4.36.0",
|
||||
"typescript": "5.7.3",
|
||||
"yaml": "2.7.0",
|
||||
"type-fest": "4.40.0",
|
||||
"typescript": "5.8.3",
|
||||
"yaml": "2.7.1",
|
||||
"yargs": "17.7.2",
|
||||
"yargs-parser": "21.1.1"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
mkdocs==1.6.1
|
||||
mkdocs-git-revision-date-localized-plugin==1.3.0
|
||||
mkdocs-material==9.6.7
|
||||
mkdocs-git-revision-date-localized-plugin==1.4.5
|
||||
mkdocs-material==9.6.12
|
||||
mkdocs-redirects==1.2.2
|
||||
mike==2.1.3
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.7.9](https://github.com/appium/appium/compare/@appium/driver-test-support@0.7.8...@appium/driver-test-support@0.7.9) (2025-04-25)
|
||||
|
||||
**Note:** Version bump only for package @appium/driver-test-support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.8](https://github.com/appium/appium/compare/@appium/driver-test-support@0.7.7...@appium/driver-test-support@0.7.8) (2025-03-17)
|
||||
|
||||
**Note:** Version bump only for package @appium/driver-test-support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.7](https://github.com/appium/appium/compare/@appium/driver-test-support@0.7.6...@appium/driver-test-support@0.7.7) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/driver-test-support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.6](https://github.com/appium/appium/compare/@appium/driver-test-support@0.7.5...@appium/driver-test-support@0.7.6) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/driver-test-support
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/driver-test-support",
|
||||
"version": "0.7.6",
|
||||
"version": "0.7.9",
|
||||
"description": "Test utilities for Appium drivers",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -41,16 +41,16 @@
|
||||
},
|
||||
"types": "./build/lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@appium/types": "^0.25.1",
|
||||
"axios": "1.8.1",
|
||||
"@appium/types": "^0.25.3",
|
||||
"axios": "1.8.4",
|
||||
"bluebird": "3.7.2",
|
||||
"chai": "5.2.0",
|
||||
"chai-as-promised": "8.0.1",
|
||||
"get-port": "5.1.1",
|
||||
"lodash": "4.17.21",
|
||||
"sinon": "19.0.2",
|
||||
"sinon": "20.0.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"type-fest": "4.36.0"
|
||||
"type-fest": "4.40.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"appium": "^2.0.0-beta.43",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.4](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@1.0.3...@appium/eslint-config-appium-ts@1.0.4) (2025-04-25)
|
||||
|
||||
**Note:** Version bump only for package @appium/eslint-config-appium-ts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.3](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@1.0.2...@appium/eslint-config-appium-ts@1.0.3) (2025-02-19)
|
||||
|
||||
**Note:** Version bump only for package @appium/eslint-config-appium-ts
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@appium/eslint-config-appium-ts",
|
||||
"type": "module",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"description": "Shared ESLint config for Appium projects (TypeScript version)",
|
||||
"keywords": [
|
||||
"eslint",
|
||||
@@ -33,7 +33,7 @@
|
||||
"@eslint/js": "^9.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.0.0 || ^10.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.0 || ^4.0.0",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-mocha": "^10.4.0",
|
||||
"eslint-plugin-promise": "^7.0.0",
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [4.0.5](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@4.0.4...@appium/execute-driver-plugin@4.0.5) (2025-04-25)
|
||||
|
||||
**Note:** Version bump only for package @appium/execute-driver-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.0.4](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@4.0.3...@appium/execute-driver-plugin@4.0.4) (2025-03-17)
|
||||
|
||||
**Note:** Version bump only for package @appium/execute-driver-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.0.3](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@4.0.2...@appium/execute-driver-plugin@4.0.3) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/execute-driver-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.0.2](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@4.0.1...@appium/execute-driver-plugin@4.0.2) (2025-02-19)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/execute-driver-plugin",
|
||||
"version": "4.0.2",
|
||||
"version": "4.0.5",
|
||||
"description": "Plugin for batching and executing driver commands with Appiums",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -41,7 +41,7 @@
|
||||
"bluebird": "3.7.2",
|
||||
"lodash": "4.17.21",
|
||||
"source-map-support": "0.5.21",
|
||||
"webdriverio": "9.10.1"
|
||||
"webdriverio": "9.12.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"appium": "^2.0.0-beta.35"
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [5.7.2](https://github.com/appium/appium/compare/@appium/fake-driver@5.7.1...@appium/fake-driver@5.7.2) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-driver
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.7.1](https://github.com/appium/appium/compare/@appium/fake-driver@5.7.0...@appium/fake-driver@5.7.1) (2025-02-19)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/fake-driver",
|
||||
"version": "5.7.1",
|
||||
"version": "5.7.2",
|
||||
"description": "Mock driver used internally by Appium for testing. Ignore",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -50,7 +50,7 @@
|
||||
"xpath": "0.0.34"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"appium": "^2.4.1"
|
||||
"appium": "^2.4.1 || ^3.0.0-beta.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.2.6](https://github.com/appium/appium/compare/@appium/fake-plugin@3.2.5...@appium/fake-plugin@3.2.6) (2025-04-25)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.2.5](https://github.com/appium/appium/compare/@appium/fake-plugin@3.2.4...@appium/fake-plugin@3.2.5) (2025-03-17)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.2.4](https://github.com/appium/appium/compare/@appium/fake-plugin@3.2.3...@appium/fake-plugin@3.2.4) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.2.3](https://github.com/appium/appium/compare/@appium/fake-plugin@3.2.2...@appium/fake-plugin@3.2.3) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-plugin
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/fake-plugin",
|
||||
"version": "3.2.3",
|
||||
"version": "3.2.6",
|
||||
"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.3.3",
|
||||
"@appium/support": "^6.0.6",
|
||||
"@appium/base-plugin": "^2.3.6",
|
||||
"@appium/support": "^6.1.0",
|
||||
"bluebird": "3.7.2",
|
||||
"lodash": "4.17.21",
|
||||
"source-map-support": "0.5.21"
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [3.1.2](https://github.com/appium/appium/compare/@appium/images-plugin@3.1.1...@appium/images-plugin@3.1.2) (2025-04-25)
|
||||
|
||||
**Note:** Version bump only for package @appium/images-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.1.1](https://github.com/appium/appium/compare/@appium/images-plugin@3.1.0...@appium/images-plugin@3.1.1) (2025-03-17)
|
||||
|
||||
**Note:** Version bump only for package @appium/images-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.1.0](https://github.com/appium/appium/compare/@appium/images-plugin@3.0.30...@appium/images-plugin@3.1.0) (2025-03-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **images-plugin:** supports image elements included in actions. ([#21055](https://github.com/appium/appium/issues/21055)) ([0c50504](https://github.com/appium/appium/commit/0c50504266eb5553dafd08bfb6161f643357114f))
|
||||
|
||||
|
||||
|
||||
## [3.0.30](https://github.com/appium/appium/compare/@appium/images-plugin@3.0.29...@appium/images-plugin@3.0.30) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/images-plugin
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
|
||||
|
||||
import _ from 'lodash';
|
||||
import {errors} from 'appium/driver';
|
||||
import {util} from '@appium/support';
|
||||
import {BasePlugin} from 'appium/plugin';
|
||||
import {compareImages} from './compare';
|
||||
import ImageElementFinder from './finder';
|
||||
@@ -83,6 +82,48 @@ export default class ImageElementPlugin extends BasePlugin {
|
||||
// otherwise just do the normal thing
|
||||
return await next();
|
||||
}
|
||||
|
||||
async performActions(next, driver, ...args) {
|
||||
// Replace with coordinates when ActionSequence includes image elements.
|
||||
const [actionSequences] = /** @type {[import('@appium/types').ActionSequence[]]} */ (args);
|
||||
for (const actionSequence of actionSequences) {
|
||||
for (const action of actionSequence.actions) {
|
||||
// The actions that can have an Element as the origin are "pointerMove" and "scroll".
|
||||
if (
|
||||
!_.isPlainObject(
|
||||
/** @type {{origin?: "viewport" | "pointer" | import('@appium/types').Element}} */ (
|
||||
action
|
||||
).origin,
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const actionWithEl =
|
||||
/** @type {import('@appium/types').PointerMoveAction | import('@appium/types').ScrollAction} */ (
|
||||
action
|
||||
);
|
||||
|
||||
const elId = util.unwrapElement(/** @type {import('@appium/types').Element} */ (actionWithEl.origin));
|
||||
if (!_.startsWith(elId, IMAGE_ELEMENT_PREFIX)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const imgEl = this.finder.getImageElement(elId);
|
||||
if (!imgEl) {
|
||||
throw new errors.NoSuchElementError();
|
||||
}
|
||||
|
||||
// Add the element's center coordinates to the offset value.
|
||||
actionWithEl.x += imgEl.center.x;
|
||||
actionWithEl.y += imgEl.center.y;
|
||||
// Set the origin to the viewport so that the external driver can process it using coordinates.
|
||||
delete actionWithEl.origin;
|
||||
}
|
||||
}
|
||||
|
||||
return await next();
|
||||
}
|
||||
}
|
||||
|
||||
export {ImageElementPlugin, getImgElFromArgs, IMAGE_STRATEGY};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/images-plugin",
|
||||
"version": "3.0.30",
|
||||
"version": "3.1.2",
|
||||
"description": "Plugin for working with images and image elements in Appium",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -34,16 +34,16 @@
|
||||
],
|
||||
"scripts": {
|
||||
"test": "npm run test:unit",
|
||||
"test:e2e": "mocha --timeout 40s --slow 20s \"./test/e2e/**/*.spec.js\"",
|
||||
"test:e2e": "mocha --timeout 40s --slow 20s \"./test/e2e/**/*.spec.*js\"",
|
||||
"test:smoke": "node ./index.js",
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.*js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/opencv": "^3.0.8",
|
||||
"@appium/support": "^6.0.6",
|
||||
"@appium/opencv": "^3.0.9",
|
||||
"@appium/support": "^6.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "10.4.3",
|
||||
"sharp": "0.33.5",
|
||||
"sharp": "0.34.1",
|
||||
"source-map-support": "0.5.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
+15
-2
@@ -2,7 +2,7 @@ import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import {remote as wdio} from 'webdriverio';
|
||||
import {MATCH_FEATURES_MODE, GET_SIMILARITY_MODE} from '../../lib/constants';
|
||||
import {TEST_IMG_1_B64, TEST_IMG_2_B64, APPSTORE_IMG_PATH} from '../fixtures';
|
||||
import {TEST_IMG_1_B64, TEST_IMG_2_B64, APPSTORE_IMG_PATH} from '../fixtures/index.cjs';
|
||||
import {pluginE2EHarness} from '@appium/plugin-test-support';
|
||||
import {tempDir, fs} from '@appium/support';
|
||||
import sharp from 'sharp';
|
||||
@@ -10,7 +10,7 @@ import sharp from 'sharp';
|
||||
const THIS_PLUGIN_DIR = path.join(__dirname, '..', '..');
|
||||
const APPIUM_HOME = path.join(THIS_PLUGIN_DIR, 'local_appium_home');
|
||||
const FAKE_DRIVER_DIR = path.join(THIS_PLUGIN_DIR, '..', 'fake-driver');
|
||||
const TEST_HOST = 'localhost';
|
||||
const TEST_HOST = '127.0.0.1';
|
||||
const TEST_PORT = 4723;
|
||||
const TEST_FAKE_APP = path.join(
|
||||
APPIUM_HOME,
|
||||
@@ -89,6 +89,19 @@ describe('ImageElementPlugin', function () {
|
||||
width.should.eql(80);
|
||||
height.should.eql(91);
|
||||
await imageEl.click();
|
||||
|
||||
const actionSequence = {
|
||||
type: 'pointer',
|
||||
id: 'mouse',
|
||||
parameters: {pointerType: 'touch'},
|
||||
actions: [
|
||||
{type: 'pointerMove', x: 0, y: 0, duration: 0, origin: imageEl},
|
||||
{type: 'pointerDown', button: 0},
|
||||
{type: 'pause', duration: 125},
|
||||
{type: 'pointerUp', button: 0},
|
||||
],
|
||||
};
|
||||
await driver.performActions([actionSequence]);
|
||||
});
|
||||
|
||||
it('should find subelements', async function () {
|
||||
Vendored
@@ -6,7 +6,7 @@ import {IMAGE_STRATEGY} from '../../lib/constants';
|
||||
import ImageElementFinder from '../../lib/finder';
|
||||
import {ImageElement} from '../../lib/image-element';
|
||||
import sinon from 'sinon';
|
||||
import {TINY_PNG, TiNY_PNG_BUF, TINY_PNG_DIMS} from '../fixtures';
|
||||
import {TINY_PNG, TiNY_PNG_BUF, TINY_PNG_DIMS} from '../fixtures/index.cjs';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const compareModule = require('../../lib/compare');
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import {ImageElementPlugin} from '../../lib/plugin';
|
||||
import {
|
||||
MATCH_FEATURES_MODE,
|
||||
@@ -6,7 +7,7 @@ import {
|
||||
IMAGE_STRATEGY,
|
||||
} from '../../lib/constants';
|
||||
import {BaseDriver} from 'appium/driver';
|
||||
import {TEST_IMG_1_B64, TEST_IMG_2_B64, TEST_IMG_2_PART_B64} from '../fixtures';
|
||||
import {TEST_IMG_1_B64, TEST_IMG_2_B64, TEST_IMG_2_PART_B64} from '../fixtures/index.cjs';
|
||||
import {util} from '@appium/support';
|
||||
|
||||
describe('ImageElementPlugin#handle', function () {
|
||||
@@ -177,4 +178,91 @@ describe('ImageElementPlugin#handle', function () {
|
||||
.should.eventually.be.rejectedWith(/not yet/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('performActions', function () {
|
||||
let imageEl;
|
||||
let nativeEl;
|
||||
before(async function () {
|
||||
imageEl = await p.findElement(next, driver, IMAGE_STRATEGY, TEST_IMG_2_PART_B64);
|
||||
nativeEl = util.wrapElement('dummy-native-element-id');
|
||||
});
|
||||
it('should replace with coords of the image elements in pointerMove, scroll actions', async function () {
|
||||
const actionSequences = [
|
||||
{
|
||||
type: 'pointer',
|
||||
id: 'mouse',
|
||||
parameters: {pointerType: 'touch'},
|
||||
actions: [
|
||||
{type: 'pointerMove', x: 0, y: 0, duration: 0, origin: imageEl},
|
||||
{type: 'pointerMove', x: 15, y: 25, duration: 0, origin: imageEl},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'wheel',
|
||||
id: 'wheel',
|
||||
actions: [
|
||||
{type: 'scroll', x: 1, y: 0, deltaX: 1, deltaY: 2, origin: imageEl},
|
||||
],
|
||||
},
|
||||
];
|
||||
await p.performActions(next, driver, actionSequences);
|
||||
actionSequences.should.eql([
|
||||
{
|
||||
type: 'pointer',
|
||||
id: 'mouse',
|
||||
parameters: {pointerType: 'touch'},
|
||||
actions: [
|
||||
{type: 'pointerMove', x: 24, y: 40, duration: 0},
|
||||
{type: 'pointerMove', x: 39, y: 65, duration: 0},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'wheel',
|
||||
id: 'wheel',
|
||||
actions: [
|
||||
{type: 'scroll', x: 25, y: 40, deltaX: 1, deltaY: 2},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
it('should not be modified except pointerMove and scroll actions includes image element as origin', async function () {
|
||||
const actionSequences = [
|
||||
{
|
||||
type: 'pointer',
|
||||
id: 'mouse',
|
||||
parameters: {pointerType: 'touch'},
|
||||
actions: [
|
||||
{type: 'pointerMove', x: 1, y: 1, duration: 0},
|
||||
{type: 'pointerMove', x: 2, y: 2, duration: 10, origin: nativeEl},
|
||||
{type: 'pointerMove', x: 3, y: 3, duration: 20, origin: 'viewport'},
|
||||
{type: 'pointerMove', x: 4, y: 4, duration: 30, origin: 'pointer'},
|
||||
{type: 'pointerDown', button: 0},
|
||||
{type: 'pause', duration: 125},
|
||||
{type: 'pointerUp', button: 0},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'wheel',
|
||||
id: 'wheel',
|
||||
actions: [
|
||||
{type: 'scroll', x: 1, y: 1, deltaX: 1, deltaY: 2},
|
||||
{type: 'scroll', x: 2, y: 2, deltaX: 2, deltaY: 3, origin: nativeEl},
|
||||
{type: 'scroll', x: 3, y: 3, deltaX: 3, deltaY: 4, origin: 'viewport'},
|
||||
{type: 'scroll', x: 4, y: 4, deltaX: 4, deltaY: 5, origin: 'pointer'},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'key',
|
||||
id: 'key',
|
||||
actions: [
|
||||
{type: 'keyDown', value: 'a'},
|
||||
{type: 'keyUp', value: 'a'},
|
||||
],
|
||||
},
|
||||
];
|
||||
const clone = _.cloneDeep(actionSequences);
|
||||
await p.performActions(next, driver, actionSequences);
|
||||
actionSequences.should.eql(clone);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,21 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.7.0](https://github.com/appium/appium/compare/@appium/logger@1.6.1...@appium/logger@1.7.0) (2025-04-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add a possibility to mask sensitive log values depending on request headers ([#21123](https://github.com/appium/appium/issues/21123)) ([c24d2ac](https://github.com/appium/appium/commit/c24d2ac46123f41ee9b54e0adefacfabd149089c))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **appium:** Return hostname as web socket url for BiDi if a broadcast address is assigned to the server ([#20603](https://github.com/appium/appium/issues/20603)) ([f0de55d](https://github.com/appium/appium/commit/f0de55da0da2fc0305876a948704c1f0a2a5990f))
|
||||
* **logger:** Error stack logging ([#21176](https://github.com/appium/appium/issues/21176)) ([1de5d0a](https://github.com/appium/appium/commit/1de5d0a0e8994b170a44eebe2d0575d5c74c3ff2))
|
||||
|
||||
|
||||
|
||||
## [1.6.1](https://github.com/appium/appium/compare/@appium/logger@1.6.0...@appium/logger@1.6.1) (2024-08-07)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import log from './lib/log';
|
||||
export { markSensitive } from './lib/log';
|
||||
export type * from './lib/types';
|
||||
|
||||
import log from './lib/log';
|
||||
export {log};
|
||||
export default log;
|
||||
|
||||
+46
-11
@@ -16,7 +16,10 @@ import type {
|
||||
import type {Writable} from 'node:stream';
|
||||
import {AsyncLocalStorage} from 'node:async_hooks';
|
||||
import { unleakString } from './utils';
|
||||
import { SecureValuesPreprocessor } from './secure-values-preprocessor';
|
||||
import {
|
||||
DEFAULT_SECURE_REPLACER,
|
||||
SecureValuesPreprocessor
|
||||
} from './secure-values-preprocessor';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
|
||||
const DEFAULT_LOG_LEVELS = [
|
||||
@@ -32,6 +35,7 @@ const DEFAULT_LOG_LEVELS = [
|
||||
['silent', Infinity],
|
||||
] as const;
|
||||
const DEFAULT_HISTORY_SIZE = 10000;
|
||||
const SENSITIVE_MESSAGE_KEY = 'f2b06625-35a2-4ed3-939a-b0b0a4abc750';
|
||||
|
||||
setBlocking(true);
|
||||
|
||||
@@ -230,16 +234,13 @@ export class Log extends EventEmitter implements Logger {
|
||||
}
|
||||
|
||||
const messageArguments: any[] = [];
|
||||
let stack: string | null = null;
|
||||
for (const formatArg of [message, ...args]) {
|
||||
messageArguments.push(formatArg);
|
||||
// resolve stack traces to a plain string.
|
||||
if (_.isError(formatArg) && formatArg.stack) {
|
||||
Object.defineProperty(formatArg, 'stack', {
|
||||
value: (stack = formatArg.stack + ''),
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
});
|
||||
let stack: string | undefined;
|
||||
for (const arg of [message, ...args]) {
|
||||
const result = this._formatLogArgument(arg);
|
||||
if (result.stack) {
|
||||
stack = result.stack;
|
||||
} else {
|
||||
messageArguments.push(result.arg);
|
||||
}
|
||||
}
|
||||
if (stack) {
|
||||
@@ -382,10 +383,44 @@ export class Log extends EventEmitter implements Logger {
|
||||
}
|
||||
}
|
||||
|
||||
private _formatLogArgument(arg: any): ArgumentFormatResult {
|
||||
const result: ArgumentFormatResult = {
|
||||
arg,
|
||||
stack: undefined,
|
||||
};
|
||||
|
||||
// mask sensitive data
|
||||
if (_.has(result.arg, SENSITIVE_MESSAGE_KEY)) {
|
||||
const { isSensitive } = this._asyncStorage.getStore() ?? {};
|
||||
result.arg = isSensitive ? DEFAULT_SECURE_REPLACER : result.arg[SENSITIVE_MESSAGE_KEY];
|
||||
}
|
||||
|
||||
// resolve stack traces to a plain string
|
||||
if (_.isError(result.arg) && result.arg.stack) {
|
||||
result.stack = result.arg.stack + '';
|
||||
Object.defineProperty(result.arg, 'stack', {
|
||||
value: result.stack,
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// this functionality has been deliberately disabled
|
||||
private clearProgress(): void {}
|
||||
private showProgress(): void {}
|
||||
}
|
||||
|
||||
export function markSensitive<T=any>(logMessage: T): {[SENSITIVE_MESSAGE_KEY]: T} {
|
||||
return {[SENSITIVE_MESSAGE_KEY]: logMessage};
|
||||
}
|
||||
|
||||
interface ArgumentFormatResult {
|
||||
arg: any,
|
||||
stack: string | undefined,
|
||||
}
|
||||
|
||||
export const GLOBAL_LOG = new Log();
|
||||
export default GLOBAL_LOG;
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
LogFilter,
|
||||
} from './types';
|
||||
|
||||
const DEFAULT_REPLACER = '**SECURE**';
|
||||
export const DEFAULT_SECURE_REPLACER = '**SECURE**';
|
||||
|
||||
/**
|
||||
* Type guard for log filter type
|
||||
@@ -42,7 +42,7 @@ export class SecureValuesPreprocessor {
|
||||
*/
|
||||
parseRule(rule: string | LogFilter): SecureValuePreprocessingRule {
|
||||
let pattern: string | undefined;
|
||||
let replacer = DEFAULT_REPLACER;
|
||||
let replacer = DEFAULT_SECURE_REPLACER;
|
||||
let flags = ['g'];
|
||||
if (_.isString(rule)) {
|
||||
if (rule.length === 0) {
|
||||
@@ -142,7 +142,7 @@ export class SecureValuesPreprocessor {
|
||||
|
||||
let result = str;
|
||||
for (const rule of this._rules) {
|
||||
result = result.replace(rule.pattern, rule.replacer ?? DEFAULT_REPLACER);
|
||||
result = result.replace(rule.pattern, rule.replacer ?? DEFAULT_SECURE_REPLACER);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/logger",
|
||||
"version": "1.6.1",
|
||||
"version": "1.7.0",
|
||||
"author": "https://github.com/appium",
|
||||
"description": "A Universal Logger For The Appium Ecosystem",
|
||||
"repository": {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/* eslint-disable no-console */
|
||||
import { Log } from '../../lib/log';
|
||||
import { Log, markSensitive} from '../../lib/log';
|
||||
import { unleakString } from '../../lib/utils';
|
||||
import {Stream} from 'node:stream';
|
||||
import _ from 'lodash';
|
||||
|
||||
describe('basic', function () {
|
||||
let chai;
|
||||
/** @type {Log} */
|
||||
let log;
|
||||
|
||||
before(async function () {
|
||||
@@ -360,6 +362,15 @@ describe('basic', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('replaces senstive messages', async function() {
|
||||
log.updateAsyncStorage({isSensitive: true}, true);
|
||||
log.log('verbose', 'test', markSensitive('log 1'));
|
||||
_.last(log.record).message.should.eql('**SECURE**');
|
||||
log.updateAsyncStorage({isSensitive: false}, true);
|
||||
log.log('verbose', 'test', markSensitive('log 1'));
|
||||
_.last(log.record).message.should.eql('log 1');
|
||||
});
|
||||
|
||||
it('max record size', function() {
|
||||
log.maxRecordSize = 3;
|
||||
log.log('verbose', 'test', 'log 1');
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.0.9](https://github.com/appium/appium/compare/@appium/opencv@3.0.8...@appium/opencv@3.0.9) (2025-04-25)
|
||||
|
||||
**Note:** Version bump only for package @appium/opencv
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.0.8](https://github.com/appium/appium/compare/@appium/opencv@3.0.7...@appium/opencv@3.0.8) (2025-02-19)
|
||||
|
||||
**Note:** Version bump only for package @appium/opencv
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/opencv",
|
||||
"version": "3.0.8",
|
||||
"version": "3.0.9",
|
||||
"description": "OpenCV-related helper library",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -44,7 +44,7 @@
|
||||
"bluebird": "3.7.2",
|
||||
"lodash": "4.17.21",
|
||||
"opencv-bindings": "4.5.5",
|
||||
"sharp": "0.33.5",
|
||||
"sharp": "0.34.1",
|
||||
"source-map-support": "0.5.21"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -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.3.53](https://github.com/appium/appium/compare/@appium/plugin-test-support@0.3.52...@appium/plugin-test-support@0.3.53) (2025-04-25)
|
||||
|
||||
**Note:** Version bump only for package @appium/plugin-test-support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.52](https://github.com/appium/appium/compare/@appium/plugin-test-support@0.3.51...@appium/plugin-test-support@0.3.52) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/plugin-test-support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.51](https://github.com/appium/appium/compare/@appium/plugin-test-support@0.3.50...@appium/plugin-test-support@0.3.51) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/plugin-test-support
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/plugin-test-support",
|
||||
"version": "0.3.51",
|
||||
"version": "0.3.53",
|
||||
"description": "Testing utilities for Appium plugins",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -41,7 +41,7 @@
|
||||
"test:unit": "exit 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/types": "^0.25.1",
|
||||
"@appium/types": "^0.25.3",
|
||||
"get-port": "5.1.1",
|
||||
"log-symbols": "4.1.0",
|
||||
"source-map-support": "0.5.21",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.1.2](https://github.com/appium/appium/compare/@appium/storage-plugin@0.1.1...@appium/storage-plugin@0.1.2) (2025-04-25)
|
||||
|
||||
**Note:** Version bump only for package @appium/storage-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.1.1](https://github.com/appium/appium/compare/@appium/storage-plugin@0.1.0...@appium/storage-plugin@0.1.1) (2025-03-17)
|
||||
|
||||
**Note:** Version bump only for package @appium/storage-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 0.1.0 (2025-03-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add storage plugin ([#21075](https://github.com/appium/appium/issues/21075)) ([ba4aa39](https://github.com/appium/appium/commit/ba4aa394d1b6676cc29644e7faa3b0590552f303))
|
||||
* **storage-plugin:** Tune the files keeping behaviour ([#21086](https://github.com/appium/appium/issues/21086)) ([15280b8](https://github.com/appium/appium/commit/15280b80d2af6b3bdf6bf2905472b05b7bca1c1d))
|
||||
@@ -0,0 +1,83 @@
|
||||
# @appium/storage-plugin
|
||||
|
||||
> Appium plugin for server-side file storage
|
||||
|
||||
This plugin adds the ability to create a dedicated storage space on the server side,
|
||||
which can be managed from the client side. This can be useful for files like application packages.
|
||||
Only one storage may exist per server process, shared by all testing sessions.
|
||||
|
||||
> [!WARNING]
|
||||
> This plugin is designed to be used with servers deployed in private networks.
|
||||
> Consider validating the setup with your security department
|
||||
> if you want to enable this plugin at a public Appium server deployment.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
appium plugin install storage
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Add the plugin name to the list of plugins to use upon server startup:
|
||||
|
||||
```bash
|
||||
appium --use-plugins=storage
|
||||
```
|
||||
|
||||
By default, the plugin creates a new temporary folder where it manages uploaded files.
|
||||
|
||||
[Refer to the Appium documentation for a list of commands supported by this plugin.](https://appium.io/docs/en/latest/commands/storage-plugin/)
|
||||
|
||||
### Storing a File
|
||||
|
||||
The procedure for storing a local file on the Appium server is as follows:
|
||||
|
||||
- Calculate the [SHA1](https://en.wikipedia.org/wiki/SHA-1) hash of the source file
|
||||
- Decide the name of the destination file in the server storage (it can be the same as the original file name)
|
||||
- Send a `POST` request to the `/storage/add` endpoint, which will return the `events` and `stream` websocket paths
|
||||
- Connect to both web sockets
|
||||
- Start listening for messages on the `events` web socket. Each message there is a JSON object wrapped
|
||||
to a string. The message must be either `{"value": {"success": true, "name":"app.ipa","sha1":"ccc963411b2621335657963322890305ebe96186"}}` to notify about a successful
|
||||
file upload, or `{"value": {"error": "<error signature>", "message": "<error message>", "traceback": "<server traceback>"}}`
|
||||
to notify about any exceptions during the upload process.
|
||||
- Start reading the source file into small chunks. The recommended size of a single chunk is 64 KB.
|
||||
- After each chunk is retrieved, pass it to the `stream` web socket.
|
||||
- After the last chunk upload is completed, either close the `stream` web
|
||||
socket to explicitly notify the server about the upload completion, or
|
||||
wait until the success event is delivered from the `events` web socket
|
||||
as soon as file hashes successfully match.
|
||||
The server must always deliver either a success or a failure
|
||||
event via the `events` web socket as described above.
|
||||
|
||||
It is also possible to upload multiple files in parallel (up to 20 jobs are supported).
|
||||
Only flat files hierarchies are supported in the storage, no subfolders are allowed.
|
||||
If a file with the same name already exists in the storage, it will be overridden with the new one.
|
||||
If a folder with the same name already exists in the storage, an error will be thrown.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
#### APPIUM_STORAGE_ROOT
|
||||
|
||||
It is also possible to customize the repository root folder path by assigning a custom path to the
|
||||
`APPIUM_STORAGE_ROOT` environment variable upon server startup. The plugin automatically deletes the
|
||||
root folder recursively upon server process termination, unless the server is
|
||||
killed forcefully. If `APPIUM_STORAGE_ROOT` points to an existing folder,
|
||||
then all files there are going to be preserved by default unless a different behavior is
|
||||
requested by [APPIUM_STORAGE_KEEP_ALL](#appium_storage_keep_all) environment variable value.
|
||||
|
||||
#### APPIUM_STORAGE_KEEP_ALL
|
||||
|
||||
If this environment variable is set to `true`, `1` or `yes` then the plugin will always keep
|
||||
storage files after the server process is terminated. All other
|
||||
values of this variable enforce the plugin to always delete all files
|
||||
from the storage folder.
|
||||
|
||||
## Examples
|
||||
|
||||
Check [integration tests](./test/e2e/storage.e2e.spec.cjs) for a working
|
||||
[WebdriverIO](https://webdriver.io/) example.
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
@@ -0,0 +1,2 @@
|
||||
export { StoragePlugin } from './plugin';
|
||||
export type * from './types';
|
||||
@@ -0,0 +1,205 @@
|
||||
import { BasePlugin } from 'appium/plugin';
|
||||
import { requireValidItemOptions, Storage, StorageArgumentError } from './storage';
|
||||
import _ from 'lodash';
|
||||
import { tempDir, fs, logger } from '@appium/support';
|
||||
import { AddRequestResult, ItemOptions, StorageItem } from './types';
|
||||
import type { Express, Request, Response } from 'express';
|
||||
import { AppiumServer } from '@appium/types';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
import WebSocket from 'ws';
|
||||
import { EventEmitter } from 'node:stream';
|
||||
import { toW3cResponseError } from './utils';
|
||||
|
||||
const log = logger.getLogger('StoragePlugin');
|
||||
|
||||
let SHARED_STORAGE: Storage | null = null;
|
||||
const STORAGE_PREFIX = '/storage';
|
||||
const WS_TTL_MS = 5 * 60 * 1000;
|
||||
const STORAGE_HANDLERS: Record<string, (req: Request, httpServer?: AppiumServer) => Promise<any>> = {};
|
||||
const STORAGE_ADDITIONS_CACHE: LRUCache<string, () => any> = new LRUCache({
|
||||
max: 20,
|
||||
ttl: WS_TTL_MS,
|
||||
dispose: (f: () => any) => f(),
|
||||
});
|
||||
|
||||
export class StoragePlugin extends BasePlugin {
|
||||
|
||||
static async updateServer(expressApp: Express, httpServer: AppiumServer): Promise<void> {
|
||||
const buildHandler = (methodName: string) => async (req: Request, res: Response) => {
|
||||
let status = 200;
|
||||
let body: any;
|
||||
try {
|
||||
const value = await STORAGE_HANDLERS[methodName](req, httpServer);
|
||||
body = {value: value ?? null};
|
||||
} catch (e) {
|
||||
[status, body] = toW3cResponseError(e);
|
||||
}
|
||||
log.debug(
|
||||
`Responding to ${methodName} with ${_.truncate(JSON.stringify(body.value), {length: 200})}`
|
||||
);
|
||||
res.set('content-type', 'application/json; charset=utf-8');
|
||||
res.status(status).send(body);
|
||||
};
|
||||
|
||||
expressApp.post(`${STORAGE_PREFIX}/add`, buildHandler(STORAGE_HANDLERS.addStorageItem.name));
|
||||
expressApp.get(`${STORAGE_PREFIX}/list`, buildHandler(STORAGE_HANDLERS.listStorageItems.name));
|
||||
expressApp.post(`${STORAGE_PREFIX}/reset`, buildHandler(STORAGE_HANDLERS.resetStorage.name));
|
||||
expressApp.post(`${STORAGE_PREFIX}/delete`, buildHandler(STORAGE_HANDLERS.deleteStorageItem.name));
|
||||
}
|
||||
}
|
||||
|
||||
STORAGE_HANDLERS.addStorageItem = async function addStorageItem(
|
||||
req: Request, httpServer: AppiumServer
|
||||
): Promise<AddRequestResult> {
|
||||
const itemOptions = requireValidItemOptions(
|
||||
parseRequestArgs(req, ['name', 'sha1']) as ItemOptions
|
||||
);
|
||||
const [stream, events] = prepareWebSockets(httpServer, itemOptions);
|
||||
return {
|
||||
ws: {
|
||||
stream,
|
||||
events,
|
||||
},
|
||||
ttlMs: WS_TTL_MS,
|
||||
};
|
||||
};
|
||||
|
||||
STORAGE_HANDLERS.listStorageItems = async function listStorageItems(): Promise<StorageItem[]> {
|
||||
return await executeStorageMethod(
|
||||
async (storage: Storage) => await storage.list()
|
||||
);
|
||||
};
|
||||
|
||||
STORAGE_HANDLERS.deleteStorageItem = async function deleteStorageItem(req: Request): Promise<boolean> {
|
||||
return await executeStorageMethod(
|
||||
async (storage: Storage) => await storage.delete(parseRequestArgs(req, ['name']).name)
|
||||
);
|
||||
};
|
||||
|
||||
STORAGE_HANDLERS.resetStorage = async function resetStorage(): Promise<void> {
|
||||
await executeStorageMethod(
|
||||
async (storage: Storage) => await storage.reset()
|
||||
);
|
||||
};
|
||||
|
||||
async function executeStorageMethod<T>(method: (storage: Storage) => Promise<T>): Promise<T> {
|
||||
const storage = await getStorageSingleton();
|
||||
return await method(storage);
|
||||
}
|
||||
|
||||
function parseRequestArgs(req: Request, requiredKeys: string[]): Record<string, any> {
|
||||
if (!_.isPlainObject(req.body)) {
|
||||
throw new StorageArgumentError(`The request body must be a valid JSON object`);
|
||||
}
|
||||
for (const key of requiredKeys) {
|
||||
if (!_.keys(req.body).includes(key)) {
|
||||
throw new StorageArgumentError(
|
||||
`The required argument '${key}' is missing (expected ${JSON.stringify(requiredKeys)})`
|
||||
);
|
||||
}
|
||||
}
|
||||
return req.body;
|
||||
}
|
||||
|
||||
function prepareWebSockets(httpServer: AppiumServer, itemOptions: ItemOptions): [string, string] {
|
||||
const commonPathname = `${STORAGE_PREFIX}/add/${itemOptions.sha1}`;
|
||||
const streamPathname = `${commonPathname}/stream`;
|
||||
const eventsPathname = `${commonPathname}/events`;
|
||||
if (!_.isEmpty(httpServer.getWebSocketHandlers(streamPathname))) {
|
||||
return [streamPathname, eventsPathname];
|
||||
}
|
||||
|
||||
const streamServer = new WebSocket.Server({
|
||||
noServer: true,
|
||||
});
|
||||
const eventsServer = new WebSocket.Server({
|
||||
noServer: true,
|
||||
});
|
||||
const signaler = new EventEmitter();
|
||||
const streamDoneCallback = () => {
|
||||
log.debug(`Unmounting stream and events web sockets at ${commonPathname}`);
|
||||
httpServer.removeWebSocketHandler(streamPathname);
|
||||
httpServer.removeWebSocketHandler(eventsPathname);
|
||||
setTimeout(() => {
|
||||
streamServer.close();
|
||||
eventsServer.close();
|
||||
signaler.removeAllListeners();
|
||||
}, 100);
|
||||
};
|
||||
STORAGE_ADDITIONS_CACHE.set(itemOptions.sha1, streamDoneCallback);
|
||||
eventsServer.on('connection', async (wsUpstream: WebSocket) => {
|
||||
signaler.on('status', (value) => wsUpstream.send(JSON.stringify(value)));
|
||||
});
|
||||
eventsServer.on('error', (e) => {
|
||||
log.info(`The ${eventsPathname} web socket server has notified about an error: ${e.message}`);
|
||||
});
|
||||
streamServer.on('connection', async (wsUpstream: WebSocket) => {
|
||||
log.info(`Starting a new server storage upload of '${itemOptions.name}' at ${streamPathname}`);
|
||||
const storage = await getStorageSingleton();
|
||||
try {
|
||||
await storage.add(itemOptions, wsUpstream);
|
||||
const successEvent = {
|
||||
value: {
|
||||
success: true,
|
||||
...itemOptions,
|
||||
}
|
||||
};
|
||||
log.debug(`Notifying about the successful addition of '${itemOptions.name}' to the server storage`);
|
||||
signaler.emit('status', successEvent);
|
||||
STORAGE_ADDITIONS_CACHE.delete(itemOptions.sha1);
|
||||
} catch (e) {
|
||||
log.debug(`Notifying about a failure while adding '${itemOptions.name}' to the server storage`);
|
||||
// in case of a failure we do not want to close the server yet
|
||||
// in anticipation of a retry
|
||||
log.error(e);
|
||||
const [, errorBody] = toW3cResponseError(e);
|
||||
signaler.emit('status', errorBody);
|
||||
}
|
||||
});
|
||||
streamServer.on('error', (e) => {
|
||||
log.info(`The ${streamPathname} web socket server has notified about an error: ${e.message}`);
|
||||
});
|
||||
httpServer.addWebSocketHandler(streamPathname, streamServer);
|
||||
httpServer.addWebSocketHandler(eventsPathname, eventsServer);
|
||||
|
||||
return [streamPathname, eventsPathname];
|
||||
}
|
||||
|
||||
const getStorageSingleton = _.memoize(async () => {
|
||||
let storageRoot: string;
|
||||
let shouldPreserveRoot = false;
|
||||
let shouldPreserveFiles = false;
|
||||
if (process.env.APPIUM_STORAGE_ROOT) {
|
||||
storageRoot = process.env.APPIUM_STORAGE_ROOT;
|
||||
shouldPreserveRoot = shouldPreserveFiles = await fs.exists(storageRoot);
|
||||
log.info(`Set '${storageRoot}' as the server storage root folder`);
|
||||
} else {
|
||||
storageRoot = await tempDir.openDir();
|
||||
log.info(`Created '${storageRoot}' as the temporary server storage root folder`);
|
||||
}
|
||||
if (process.env.APPIUM_STORAGE_KEEP_ALL) {
|
||||
shouldPreserveFiles = ['true', '1', 'yes'].includes(_.toLower(process.env.APPIUM_STORAGE_KEEP_ALL));
|
||||
}
|
||||
if (shouldPreserveFiles) {
|
||||
log.info(`All server storage items will be always preserved unless deleted explicitly`);
|
||||
} else {
|
||||
log.info(
|
||||
`All server storage items will be cleaned up automatically from '${storageRoot}' after ` +
|
||||
`Appium server termination`
|
||||
);
|
||||
}
|
||||
SHARED_STORAGE = new Storage(
|
||||
storageRoot,
|
||||
shouldPreserveRoot,
|
||||
shouldPreserveFiles,
|
||||
log,
|
||||
);
|
||||
await SHARED_STORAGE.reset();
|
||||
return SHARED_STORAGE;
|
||||
});
|
||||
|
||||
process.once('exit', () => {
|
||||
SHARED_STORAGE?.cleanupSync();
|
||||
});
|
||||
|
||||
export default StoragePlugin;
|
||||
@@ -0,0 +1,276 @@
|
||||
import { fs, timing } from '@appium/support';
|
||||
import _ from 'lodash';
|
||||
import B from 'bluebird';
|
||||
import type { Path } from 'path-scurry';
|
||||
import path from 'node:path';
|
||||
import type { Stats } from 'node:fs';
|
||||
import nativeFs from 'node:fs';
|
||||
import { rimrafSync } from 'rimraf';
|
||||
import { ItemOptions, StorageItem } from './types';
|
||||
import AsyncLock from 'async-lock';
|
||||
import type { AppiumLogger } from '@appium/types';
|
||||
import type Stream from 'node:stream';
|
||||
import type WebSocket from 'ws';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
const MAX_TASKS = 5;
|
||||
const TMP_EXT = '.filepart';
|
||||
const ADDITION_LOCK = new AsyncLock();
|
||||
const WS_SERVER_ERROR = 1011;
|
||||
const SHA1_HASH_LEN = 40;
|
||||
|
||||
|
||||
export class Storage {
|
||||
private readonly _root: string;
|
||||
private readonly _log: AppiumLogger;
|
||||
private readonly _shouldPreserveRoot: boolean;
|
||||
private readonly _shouldPreserveFiles: boolean;
|
||||
|
||||
constructor(
|
||||
root: string,
|
||||
shouldPreserveRoot: boolean,
|
||||
shouldPreserveFiles: boolean,
|
||||
log: AppiumLogger,
|
||||
) {
|
||||
this._root = root;
|
||||
this._log = log;
|
||||
this._shouldPreserveRoot = shouldPreserveRoot;
|
||||
this._shouldPreserveFiles = shouldPreserveFiles;
|
||||
}
|
||||
|
||||
async list(): Promise<StorageItem[]> {
|
||||
const items = (await this._listFiles())
|
||||
.filter((p) => !p.fullpath().endsWith(TMP_EXT));
|
||||
if (_.isEmpty(items)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const statPromises: B<Stats>[] = [];
|
||||
for (const item of items) {
|
||||
const pending = statPromises.filter((p) => p.isPending());
|
||||
if (pending.length >= MAX_TASKS) {
|
||||
await B.any(pending);
|
||||
}
|
||||
statPromises.push(B.resolve(fs.stat(item.fullpath())));
|
||||
}
|
||||
return _.zip(
|
||||
items.map((f) => f.fullpath()),
|
||||
await B.all(statPromises)
|
||||
)
|
||||
.map(([fullpath, stat]) => ({
|
||||
name: path.basename(fullpath as string),
|
||||
path: fullpath as string,
|
||||
size: (stat as Stats).size,
|
||||
}));
|
||||
}
|
||||
|
||||
async add(opts: ItemOptions, source: Stream | WebSocket): Promise<void> {
|
||||
const {name} = requireValidItemOptions(opts);
|
||||
// _.toLower is needed for case-insensitive server filesystems
|
||||
await ADDITION_LOCK.acquire(_.toLower(name), async () => {
|
||||
if (_.isFunction((source as any).pipe)) {
|
||||
await this._addFromStream(opts, source as Stream);
|
||||
} else {
|
||||
await this._addFromWebSocket(opts, source as WebSocket);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async delete(name: string): Promise<boolean> {
|
||||
if (_.toLower(name).endsWith(TMP_EXT)) {
|
||||
return false;
|
||||
}
|
||||
const destinationPath = path.join(this._root, name);
|
||||
if (!await fs.exists(destinationPath)) {
|
||||
return false;
|
||||
}
|
||||
await fs.rimraf(destinationPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
async reset(): Promise<void> {
|
||||
if (!this._shouldPreserveRoot && !this._shouldPreserveFiles) {
|
||||
await fs.rimraf(this._root);
|
||||
}
|
||||
|
||||
if (!await fs.exists(this._root)) {
|
||||
await fs.mkdirp(this._root);
|
||||
return;
|
||||
}
|
||||
|
||||
const files = (await this._listFiles())
|
||||
.map((p) => p.fullpath())
|
||||
.filter(
|
||||
(fullPath) => !this._shouldPreserveFiles || _.toLower(path.basename(fullPath)).endsWith(TMP_EXT)
|
||||
);
|
||||
if (_.isEmpty(files)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promises: B<any>[] = [];
|
||||
for (const fullPath of files) {
|
||||
const pending = promises.filter((p) => p.isPending());
|
||||
if (pending.length >= MAX_TASKS) {
|
||||
await B.any(pending);
|
||||
}
|
||||
promises.push(B.resolve(fs.rimraf(fullPath)));
|
||||
}
|
||||
await B.all(promises);
|
||||
}
|
||||
|
||||
cleanupSync(): void {
|
||||
this._log.debug(`Cleaning up the '${this._root}' server storage folder`);
|
||||
|
||||
if (!this._shouldPreserveRoot && !this._shouldPreserveFiles) {
|
||||
rimrafSync(this._root);
|
||||
return;
|
||||
}
|
||||
|
||||
let itemNames: string[];
|
||||
try {
|
||||
itemNames = nativeFs.readdirSync(this._root)
|
||||
.filter((name) => !_.startsWith(name, '.'));
|
||||
} catch (e) {
|
||||
this._log.warn(
|
||||
`Cannot list the '${this._root}' server storage folder. Original error: ${e.message}. ` +
|
||||
`Skipping the cleanup.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_.isEmpty(itemNames)) {
|
||||
if (!this._shouldPreserveRoot) {
|
||||
rimrafSync(this._root);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const matchedNames = itemNames.filter(
|
||||
(name) => !this._shouldPreserveFiles || _.toLower(name).endsWith(TMP_EXT)
|
||||
);
|
||||
for (const matchedName of matchedNames) {
|
||||
rimrafSync(path.join(this._root, matchedName));
|
||||
}
|
||||
if (!this._shouldPreserveRoot && _.isEmpty(_.without(itemNames, ...matchedNames))) {
|
||||
rimrafSync(this._root);
|
||||
}
|
||||
}
|
||||
|
||||
private async _listFiles(): Promise<Path[]> {
|
||||
const paths = (await fs.glob('*', {
|
||||
cwd: this._root,
|
||||
withFileTypes: true,
|
||||
})) as unknown as Path[];
|
||||
return paths.filter((item) => item.isFile());
|
||||
}
|
||||
|
||||
private async _addFromStream(opts: ItemOptions, source: Stream): Promise<void> {
|
||||
const {name} = opts;
|
||||
const fullPath = path.join(this._root, toTempName(name));
|
||||
const timer = new timing.Timer().start();
|
||||
const destination = fs.createWriteStream(fullPath);
|
||||
source.pipe(destination);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
destination.once('finish', () => resolve(true));
|
||||
source.once('error', reject);
|
||||
destination.once('error', reject);
|
||||
});
|
||||
await this._finalizeItem(opts, timer, fullPath, await fs.hash(fullPath));
|
||||
} catch (e) {
|
||||
await fs.rimraf(fullPath);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async _addFromWebSocket(opts: ItemOptions, source: WebSocket): Promise<void> {
|
||||
const {name, sha1} = opts;
|
||||
const fullPath = path.join(this._root, toTempName(name));
|
||||
const timer = new timing.Timer().start();
|
||||
const destination = fs.createWriteStream(fullPath);
|
||||
const sha1sum = createHash('sha1');
|
||||
let didDigestMatch = false;
|
||||
let recentDigest: string | null = null;
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
source.on('message', (data: WebSocket.RawData) => {
|
||||
if (didDigestMatch) {
|
||||
// ignore further chunks if hashes have already matched
|
||||
return;
|
||||
}
|
||||
destination.write(data, (e) => {
|
||||
if (e) {
|
||||
source.close(WS_SERVER_ERROR);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
sha1sum.update(data as any);
|
||||
recentDigest = sha1sum.copy().digest('hex');
|
||||
if (_.toLower(recentDigest) === _.toLower(sha1)) {
|
||||
didDigestMatch = true;
|
||||
destination.close(() => resolve(true));
|
||||
}
|
||||
});
|
||||
source.once('close', () => {
|
||||
destination.close(() => resolve(true));
|
||||
});
|
||||
source.once('error', reject);
|
||||
destination.once('error', (e) => {
|
||||
source.close(WS_SERVER_ERROR);
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
await this._finalizeItem(opts, timer, fullPath, recentDigest ?? sha1sum.digest('hex'));
|
||||
} catch (e) {
|
||||
await fs.rimraf(fullPath);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async _finalizeItem(
|
||||
opts: ItemOptions,
|
||||
timer: timing.Timer,
|
||||
fullPath: string,
|
||||
actualHashDigest: string,
|
||||
): Promise<void> {
|
||||
const {name, sha1} = opts;
|
||||
this._log.info(
|
||||
`'${name}' has been added to the server storage within ` +
|
||||
`${timer.getDuration().asMilliSeconds}ms. Verifying hashes.`
|
||||
);
|
||||
if (_.toLower(actualHashDigest) !== _.toLower(sha1)) {
|
||||
throw new StorageArgumentError(
|
||||
`The actual SHA1 hash value '${actualHashDigest}' must be equal ` +
|
||||
`to the expected hash value of '${sha1}' for '${name}'`
|
||||
);
|
||||
}
|
||||
await fs.mv(fullPath, path.join(this._root, name));
|
||||
}
|
||||
}
|
||||
|
||||
function toTempName(origName: string): string {
|
||||
return `${origName}${TMP_EXT}`;
|
||||
}
|
||||
|
||||
export function requireValidItemOptions(opts: ItemOptions): ItemOptions {
|
||||
if (_.isEmpty(opts.name)) {
|
||||
throw new StorageArgumentError(`The provided file name '${opts.name}' must not be empty`);
|
||||
}
|
||||
const sanitizedName = fs.sanitizeName(opts.name, {
|
||||
replacement: '_',
|
||||
});
|
||||
if (opts.name !== sanitizedName) {
|
||||
throw new StorageArgumentError(
|
||||
`The provided name value '${opts.name}' must be a valid file name. ` +
|
||||
`Did you mean '${sanitizedName}'?`
|
||||
);
|
||||
}
|
||||
if (!_.isString(opts.sha1) || opts.sha1.length !== SHA1_HASH_LEN) {
|
||||
throw new StorageArgumentError(
|
||||
`The provided hash value '${opts.sha1}' must be a valid SHA1 string, for ` +
|
||||
`example 'ccc963411b2621335657963322890305ebe96186'`
|
||||
);
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
export class StorageArgumentError extends Error {}
|
||||
@@ -0,0 +1,29 @@
|
||||
export interface ItemOptions {
|
||||
/** Destination file name */
|
||||
name: string;
|
||||
/** SHA1 hash of the file */
|
||||
sha1: string;
|
||||
}
|
||||
|
||||
export interface StorageItem {
|
||||
/** Item name */
|
||||
name: string;
|
||||
/** Full path to the item on the server FS */
|
||||
path: string;
|
||||
/** Item size in bytes */
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface WsEndpoints {
|
||||
/** Websocket pathname used to upload file chunks */
|
||||
stream: string;
|
||||
/** Websocket pathname used to deliver events to the client */
|
||||
events: string;
|
||||
}
|
||||
|
||||
export interface AddRequestResult {
|
||||
/** The websocket pathname where the further upload is expected */
|
||||
ws: WsEndpoints;
|
||||
/** For how long the websocket server is going to be alive in milliseconds */
|
||||
ttlMs: number;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import _ from 'lodash';
|
||||
import { errors, getResponseForW3CError } from 'appium/driver';
|
||||
|
||||
|
||||
export function toW3cResponseError(err: any): [number, Record<string, any>] {
|
||||
if (_.isFunction(getResponseForW3CError)) {
|
||||
return getResponseForW3CError(err) as [number, Record<string, any>];
|
||||
}
|
||||
// TODO: deprecate the below after the plugin only supports Appium3+
|
||||
const protocolError = _.has(err, 'w3cStatus') ? err : new errors.UnknownError(err);
|
||||
return [protocolError.w3cStatus, {
|
||||
value: {
|
||||
error: protocolError.error,
|
||||
message: protocolError.message,
|
||||
stacktrace: protocolError.stacktrace || protocolError.stack,
|
||||
},
|
||||
}];
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "@appium/storage-plugin",
|
||||
"version": "0.1.2",
|
||||
"description": "Server-side storage plugin",
|
||||
"keywords": [
|
||||
"automation",
|
||||
"javascript",
|
||||
"selenium",
|
||||
"webdriver",
|
||||
"ios",
|
||||
"android",
|
||||
"firefoxos",
|
||||
"testing"
|
||||
],
|
||||
"homepage": "https://appium.io",
|
||||
"bugs": {
|
||||
"url": "https://github.com/appium/appium/issues"
|
||||
},
|
||||
"main": "./build/lib/index.js",
|
||||
"types": "./build/lib/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appium/appium.git",
|
||||
"directory": "packages/storage-plugin"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "https://github.com/appium",
|
||||
"directories": {
|
||||
"lib": "./lib"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"build",
|
||||
"tsconfig.json",
|
||||
"!build/tsconfig.tsbuildinfo"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "npm run test:unit",
|
||||
"test:smoke": "node ./build/lib/index.js",
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.*js\"",
|
||||
"test:e2e": "mocha --timeout 20s --slow 10s \"./test/e2e/**/*.spec.*js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^6.1.0",
|
||||
"async-lock": "1.4.1",
|
||||
"bluebird": "3.7.2",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "10.4.3",
|
||||
"rimraf": "5.0.10",
|
||||
"source-map-support": "0.5.21",
|
||||
"ws": "8.18.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"appium": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
"npm": ">=8"
|
||||
},
|
||||
"appium": {
|
||||
"pluginName": "storage",
|
||||
"mainClass": "StoragePlugin"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"tags": [
|
||||
"appium"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
import {remote as wdio} from 'webdriverio';
|
||||
import {pluginE2EHarness} from '@appium/plugin-test-support';
|
||||
import {tempDir, fs} from '@appium/support';
|
||||
import axios from 'axios';
|
||||
import { WebSocket } from 'ws';
|
||||
|
||||
const BUFFER_SIZE = 0xFFFF;
|
||||
const THIS_PLUGIN_DIR = path.join(__dirname, '..', '..');
|
||||
const APPIUM_HOME = path.join(THIS_PLUGIN_DIR, 'local_appium_home');
|
||||
const FAKE_DRIVER_DIR = path.join(THIS_PLUGIN_DIR, '..', 'fake-driver');
|
||||
const TEST_HOST = '127.0.0.1';
|
||||
const TEST_PORT = 4725;
|
||||
const TEST_FAKE_APP = path.join(
|
||||
APPIUM_HOME,
|
||||
'node_modules',
|
||||
'@appium',
|
||||
'fake-driver',
|
||||
'test',
|
||||
'fixtures',
|
||||
'app.xml'
|
||||
);
|
||||
const TEST_CAPS = {
|
||||
platformName: 'Fake',
|
||||
'appium:automationName': 'Fake',
|
||||
'appium:deviceName': 'Fake',
|
||||
'appium:app': TEST_FAKE_APP,
|
||||
};
|
||||
const WDIO_OPTS = {
|
||||
hostname: TEST_HOST,
|
||||
port: TEST_PORT,
|
||||
connectionRetryCount: 0,
|
||||
capabilities: TEST_CAPS,
|
||||
};
|
||||
|
||||
describe('StoragePlugin', function () {
|
||||
let server;
|
||||
let driver;
|
||||
/** @type {string | undefined | undefined} */
|
||||
let storageRoot;
|
||||
|
||||
beforeEach(async function () {
|
||||
storageRoot = await tempDir.openDir();
|
||||
driver = await wdio(WDIO_OPTS);
|
||||
const baseUrl = `http://${TEST_HOST}:${TEST_PORT}/storage`;
|
||||
driver.addCommand(
|
||||
'addStorageItem',
|
||||
async (name, sha1) => (await axios.post(
|
||||
`${baseUrl}/add`, {name, sha1}
|
||||
)).data.value
|
||||
);
|
||||
driver.addCommand(
|
||||
'listStorageItems',
|
||||
async () => (await axios.get(`${baseUrl}/list`)).data.value
|
||||
);
|
||||
driver.addCommand(
|
||||
'resetStorageItems',
|
||||
async () => (await axios.post(`${baseUrl}/reset`)).data.value
|
||||
);
|
||||
driver.addCommand(
|
||||
'deleteStorageItem',
|
||||
async (name) => (await axios.post(`${baseUrl}/delete`, {name})).data.value
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
if (driver) {
|
||||
await driver.deleteSession();
|
||||
driver = null;
|
||||
}
|
||||
if (storageRoot && await fs.exists(storageRoot)) {
|
||||
await fs.rimraf(storageRoot);
|
||||
storageRoot = null;
|
||||
}
|
||||
});
|
||||
|
||||
pluginE2EHarness({
|
||||
before,
|
||||
after,
|
||||
server,
|
||||
port: TEST_PORT,
|
||||
host: TEST_HOST,
|
||||
appiumHome: APPIUM_HOME,
|
||||
driverName: 'fake',
|
||||
driverSource: 'local',
|
||||
driverSpec: FAKE_DRIVER_DIR,
|
||||
pluginName: 'storage',
|
||||
pluginSource: 'local',
|
||||
pluginSpec: THIS_PLUGIN_DIR,
|
||||
});
|
||||
|
||||
it('should manage storage files', async function () {
|
||||
let items = await driver.listStorageItems();
|
||||
_.isEmpty(items).should.be.true;
|
||||
const name1 = path.basename('foo1.bar');
|
||||
const name2 = path.basename('foo2.bar');
|
||||
const pkgPath = path.join(__dirname, '..', '..', 'package.json');
|
||||
await Promise.all([
|
||||
addFileToStorage(TEST_FAKE_APP, name1),
|
||||
addFileToStorage(pkgPath, name2),
|
||||
]);
|
||||
items = await driver.listStorageItems();
|
||||
items.length.should.eql(2);
|
||||
_.isEqual(new Set(items.map(({name}) => name)), new Set([name1, name2])).should.be.true;
|
||||
const isDeleted = await driver.deleteStorageItem(name1);
|
||||
isDeleted.should.be.true;
|
||||
items = await driver.listStorageItems();
|
||||
items.length.should.eql(1);
|
||||
items[0].name.should.eql(name2);
|
||||
await driver.resetStorageItems();
|
||||
items = await driver.listStorageItems();
|
||||
items.length.should.eql(0);
|
||||
});
|
||||
|
||||
async function addFileToStorage(sourcePath, name) {
|
||||
const hash = await fs.hash(sourcePath);
|
||||
const {size} = await fs.stat(sourcePath);
|
||||
const {ws: {events, stream}} = await driver.addStorageItem(name, hash, sourcePath);
|
||||
const streamWs = new WebSocket(`ws://${TEST_HOST}:${TEST_PORT}${stream}`);
|
||||
const eventsWs = new WebSocket(`ws://${TEST_HOST}:${TEST_PORT}${events}`);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
streamWs.once('error', reject);
|
||||
eventsWs.once('error', reject);
|
||||
eventsWs.once('message', async (data) => {
|
||||
let strData;
|
||||
if (_.isBuffer(data)) {
|
||||
strData = data.toString();
|
||||
} else if (_.isString(data)) {
|
||||
strData = data;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const {value} = JSON.parse(strData);
|
||||
if (value?.success) {
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error(JSON.stringify(value)));
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
streamWs.once('open', async () => {
|
||||
const fhandle = await fs.openFile(sourcePath, 'r');
|
||||
try {
|
||||
let bytesRead = 0;
|
||||
while (bytesRead < size) {
|
||||
const bufferSize = Math.min(BUFFER_SIZE, size - bytesRead);
|
||||
const buffer = Buffer.alloc(bufferSize);
|
||||
await fhandle.read(buffer, 0, bufferSize, bytesRead);
|
||||
streamWs.send(buffer);
|
||||
bytesRead += bufferSize;
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
} finally {
|
||||
await fhandle.close();
|
||||
streamWs.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
streamWs.close();
|
||||
eventsWs.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import _ from 'lodash';
|
||||
import { Storage } from '../../lib/storage';
|
||||
import { tempDir, fs, logger } from '@appium/support';
|
||||
import path from 'node:path';
|
||||
|
||||
const log = logger.getLogger();
|
||||
|
||||
describe('storage', function () {
|
||||
/** @type {string | undefined} */
|
||||
let tmpRoot;
|
||||
/** @type {Storage | undefined | null} */
|
||||
let storage;
|
||||
/** @type {string | undefined | null} */
|
||||
let storageRoot;
|
||||
|
||||
before(async function () {
|
||||
const chai = await import('chai');
|
||||
const chaiAsPromised = await import('chai-as-promised');
|
||||
chai.use(chaiAsPromised.default);
|
||||
chai.should();
|
||||
tmpRoot = await tempDir.openDir();
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
if (tmpRoot && await fs.exists(tmpRoot)) {
|
||||
await fs.rimraf(tmpRoot);
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
storageRoot = await tempDir.openDir();
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
if (storage) {
|
||||
await storage.reset();
|
||||
storage = null;
|
||||
}
|
||||
if (storageRoot && await fs.exists(storageRoot)) {
|
||||
await fs.rimraf(storageRoot);
|
||||
storageRoot = null;
|
||||
}
|
||||
});
|
||||
|
||||
it('should be initially empty', async function () {
|
||||
storage = new Storage(storageRoot, false, false, log);
|
||||
const files = await storage.list();
|
||||
_.isEmpty(files).should.be.true;
|
||||
(await storage.delete('foo')).should.be.false;
|
||||
});
|
||||
|
||||
it('should reset all files if shouldPreserveFiles is not requested', async function () {
|
||||
const name = 'foo.bar';
|
||||
const tmpName = 'bar.baz.filepart';
|
||||
await fs.writeFile(path.join(storageRoot, name), Buffer.alloc(1));
|
||||
await fs.writeFile(path.join(storageRoot, tmpName), Buffer.alloc(1));
|
||||
storage = new Storage(storageRoot, true, false, log);
|
||||
const files = await storage.list();
|
||||
files.length.should.eql(1);
|
||||
await storage.reset();
|
||||
(await fs.exists(path.join(storageRoot, name))).should.be.false;
|
||||
(await fs.exists(path.join(storageRoot, tmpName))).should.be.false;
|
||||
});
|
||||
|
||||
it('should only reset parital files if shouldPreserveFiles requested', async function () {
|
||||
const name = 'foo.bar';
|
||||
const tmpName = 'bar.baz.filepart';
|
||||
await fs.writeFile(path.join(storageRoot, name), Buffer.alloc(1));
|
||||
await fs.writeFile(path.join(storageRoot, tmpName), Buffer.alloc(1));
|
||||
storage = new Storage(storageRoot, true, true, log);
|
||||
let files = await storage.list();
|
||||
files.length.should.eql(1);
|
||||
await storage.reset();
|
||||
files = await storage.list();
|
||||
files.length.should.eql(1);
|
||||
(await fs.exists(path.join(storageRoot, tmpName))).should.be.false;
|
||||
});
|
||||
|
||||
it('should perform basic operations', async function () {
|
||||
storage = new Storage(storageRoot, false, false, log);
|
||||
const name = 'foo.bar';
|
||||
const size = 1 * 1024 * 1024;
|
||||
await addFileToStorage(name, size);
|
||||
let files = await storage.list();
|
||||
_.isEmpty(files).should.be.false;
|
||||
files[0].name.should.eql(name);
|
||||
files[0].size.should.eql(size);
|
||||
files[0].path.should.eql(path.join(storageRoot, name));
|
||||
(await storage.delete(name)).should.be.true;
|
||||
files = await storage.list();
|
||||
_.isEmpty(files).should.be.true;
|
||||
});
|
||||
|
||||
it('should be reset and preserve the root', async function () {
|
||||
storage = new Storage(storageRoot, true, false, log);
|
||||
const name = 'foo.bar';
|
||||
const size = 1 * 1024 * 1024;
|
||||
await addFileToStorage(name, size);
|
||||
await storage.reset();
|
||||
const files = await storage.list();
|
||||
_.isEmpty(files).should.be.true;
|
||||
(await fs.exists(storageRoot)).should.be.true;
|
||||
});
|
||||
|
||||
it('should be reset and preserve items', async function () {
|
||||
storage = new Storage(storageRoot, false, true, log);
|
||||
const name = 'foo.bar';
|
||||
const size = 1 * 1024 * 1024;
|
||||
await addFileToStorage(name, size);
|
||||
await storage.reset();
|
||||
const files = await storage.list();
|
||||
_.isEmpty(files).should.be.false;
|
||||
(await fs.exists(storageRoot)).should.be.true;
|
||||
});
|
||||
|
||||
async function addFileToStorage(name, size) {
|
||||
const dummyPath = path.join(tmpRoot, name);
|
||||
await fs.writeFile(dummyPath, Buffer.alloc(size));
|
||||
const sha1 = await fs.hash(dummyPath);
|
||||
await storage.add({name, sha1}, fs.createReadStream(dummyPath));
|
||||
return dummyPath;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "build",
|
||||
"checkJs": true
|
||||
},
|
||||
"extends": "@appium/tsconfig/tsconfig.plugin.json",
|
||||
"include": ["lib"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../base-plugin"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
## [6.1.0](https://github.com/appium/appium/compare/@appium/support@6.0.8...@appium/support@6.1.0) (2025-04-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add a possibility to mask sensitive log values depending on request headers ([#21123](https://github.com/appium/appium/issues/21123)) ([c24d2ac](https://github.com/appium/appium/commit/c24d2ac46123f41ee9b54e0adefacfabd149089c))
|
||||
|
||||
|
||||
|
||||
## [6.0.8](https://github.com/appium/appium/compare/@appium/support@6.0.7...@appium/support@6.0.8) (2025-03-17)
|
||||
|
||||
**Note:** Version bump only for package @appium/support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [6.0.7](https://github.com/appium/appium/compare/@appium/support@6.0.6...@appium/support@6.0.7) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [6.0.6](https://github.com/appium/appium/compare/@appium/support@6.0.5...@appium/support@6.0.6) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/support
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import globalLog from '@appium/logger';
|
||||
import globalLog, { markSensitive as _markSensitive } from '@appium/logger';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
@@ -84,6 +84,23 @@ export function getLogger(prefix = null) {
|
||||
return /** @type {AppiumLogger} */ (wrappedLogger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks arbitrary log message as sensitive.
|
||||
* This message will then be replaced with the default replacer
|
||||
* while being logged by any `info`, `debug`, etc. methods if the
|
||||
* asyncStorage has `isSensitive` flag enabled in its async context.
|
||||
* The latter is enabled by the corresponding HTTP middleware
|
||||
* in response to the `X-Appium-Is-Sensitive` request header
|
||||
* being set to 'true'.
|
||||
*
|
||||
* @template {any} T
|
||||
* @param {T} logMessage
|
||||
* @returns {{[k: string]: T}}
|
||||
*/
|
||||
export function markSensitive(logMessage) {
|
||||
return _markSensitive(logMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {[import('@appium/logger').Logger, boolean]}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/support",
|
||||
"version": "6.0.6",
|
||||
"version": "6.1.0",
|
||||
"description": "Support libs used across appium packages",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -41,12 +41,12 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/logger": "^1.6.1",
|
||||
"@appium/tsconfig": "^0.3.4",
|
||||
"@appium/types": "^0.25.1",
|
||||
"@appium/logger": "^1.7.0",
|
||||
"@appium/tsconfig": "^0.3.5",
|
||||
"@appium/types": "^0.25.3",
|
||||
"@colors/colors": "1.6.0",
|
||||
"archiver": "7.0.1",
|
||||
"axios": "1.8.1",
|
||||
"axios": "1.8.4",
|
||||
"base64-stream": "1.0.0",
|
||||
"bluebird": "3.7.2",
|
||||
"bplist-creator": "0.1.1",
|
||||
@@ -73,13 +73,13 @@
|
||||
"source-map-support": "0.5.21",
|
||||
"supports-color": "8.1.1",
|
||||
"teen_process": "2.3.1",
|
||||
"type-fest": "4.36.0",
|
||||
"type-fest": "4.40.0",
|
||||
"uuid": "11.1.0",
|
||||
"which": "4.0.0",
|
||||
"yauzl": "3.2.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sharp": "0.33.5"
|
||||
"sharp": "0.34.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.1.9](https://github.com/appium/appium/compare/@appium/test-support@3.1.8...@appium/test-support@3.1.9) (2025-04-25)
|
||||
|
||||
**Note:** Version bump only for package @appium/test-support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.1.8](https://github.com/appium/appium/compare/@appium/test-support@3.1.7...@appium/test-support@3.1.8) (2025-03-17)
|
||||
|
||||
**Note:** Version bump only for package @appium/test-support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.1.7](https://github.com/appium/appium/compare/@appium/test-support@3.1.6...@appium/test-support@3.1.7) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/test-support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.1.6](https://github.com/appium/appium/compare/@appium/test-support@3.1.5...@appium/test-support@3.1.6) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/test-support
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import B from 'bluebird';
|
||||
import sinon from 'sinon';
|
||||
|
||||
/**
|
||||
@@ -68,7 +67,7 @@ export class MockStore {
|
||||
if (this.#mocks) {
|
||||
throw new ReferenceError('Cannot create mocks twice; call `reset()` first.');
|
||||
}
|
||||
this.sandbox = this.sandbox ?? sinon.createSandbox().usingPromise(B);
|
||||
this.sandbox = this.sandbox ?? sinon.createSandbox();
|
||||
for (const [key, value] of Object.entries(mockDefs)) {
|
||||
this[key] = this.sandbox.mock(value);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import sinon from 'sinon';
|
||||
import _ from 'lodash';
|
||||
import B from 'bluebird';
|
||||
import {MockStore} from './mock-utils';
|
||||
|
||||
/**
|
||||
@@ -59,7 +58,7 @@ export class SandboxStore {
|
||||
* @param {Mocks} mocks
|
||||
*/
|
||||
createSandbox(mocks = /** @type {Mocks} */ ({})) {
|
||||
this.sandbox = this.sandbox ?? sinon.createSandbox().usingPromise(B);
|
||||
this.sandbox = this.sandbox ?? sinon.createSandbox();
|
||||
this.mocks = new MockStore(this.sandbox).createMocks(mocks);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/test-support",
|
||||
"version": "3.1.6",
|
||||
"version": "3.1.9",
|
||||
"description": "A collection of test utilities used across Appium packages",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -45,12 +45,12 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^6.0.6",
|
||||
"@appium/support": "^6.1.0",
|
||||
"@colors/colors": "1.6.0",
|
||||
"bluebird": "3.7.2",
|
||||
"lodash": "4.17.21",
|
||||
"loud-rejection": "2.2.0",
|
||||
"sinon": "19.0.2",
|
||||
"sinon": "20.0.0",
|
||||
"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.5](https://github.com/appium/appium/compare/@appium/tsconfig@0.3.4...@appium/tsconfig@0.3.5) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/tsconfig
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.4](https://github.com/appium/appium/compare/@appium/tsconfig@0.3.3...@appium/tsconfig@0.3.4) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/tsconfig
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/tsconfig",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"description": "Shared TypeScript Config for Appium",
|
||||
"main": "tsconfig.json",
|
||||
"keywords": [
|
||||
@@ -32,7 +32,7 @@
|
||||
"npm": ">=8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tsconfig/node14": "14.1.2"
|
||||
"@tsconfig/node14": "14.1.3"
|
||||
},
|
||||
"scripts": {
|
||||
"test:smoke": "node tsconfig.json && node tsconfig.plugin.json"
|
||||
|
||||
@@ -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.25.3](https://github.com/appium/appium/compare/@appium/types@0.25.2...@appium/types@0.25.3) (2025-04-25)
|
||||
|
||||
**Note:** Version bump only for package @appium/types
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.25.2](https://github.com/appium/appium/compare/@appium/types@0.25.1...@appium/types@0.25.2) (2025-03-11)
|
||||
|
||||
**Note:** Version bump only for package @appium/types
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.25.1](https://github.com/appium/appium/compare/@appium/types@0.25.0...@appium/types@0.25.1) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/types
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user