mirror of
https://github.com/appium/appium.git
synced 2026-11-08 02:38:07 +08:00
Compare commits
141
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
0b1b1a4776 | ||
|
|
d98665ab20 | ||
|
|
2171aaa000 | ||
|
|
a3196e3108 | ||
|
|
aeba9fd2d6 | ||
|
|
97c4d05be8 | ||
|
|
c4d8ab45c3 | ||
|
|
63e4eccb4a | ||
|
|
1178eef6b8 | ||
|
|
e67e210df2 | ||
|
|
d68fe942be | ||
|
|
dcd6b8f141 | ||
|
|
c8ad6c05f5 | ||
|
|
554114203f | ||
|
|
594bc04c03 | ||
|
|
ca8590221e | ||
|
|
4b75153659 | ||
|
|
2d9282bf22 | ||
|
|
0d1150943d | ||
|
|
b9a586d336 | ||
|
|
7a4fbb2ab3 | ||
|
|
e7ac01ca3d | ||
|
|
53a0520887 | ||
|
|
0228ffafb8 | ||
|
|
25acb30e9a | ||
|
|
dbd2e51556 | ||
|
|
0bfe9d69de | ||
|
|
e473b05130 | ||
|
|
dd64a48fca | ||
|
|
4473ef4da8 | ||
|
|
f9f88e66b6 | ||
|
|
715fd03c45 | ||
|
|
8a664a4eef | ||
|
|
dc24864cbb | ||
|
|
c102d8fd8e | ||
|
|
703085bc54 | ||
|
|
47497bc8da | ||
|
|
ea00e3aa18 | ||
|
|
f8ddac629d | ||
|
|
eb1b156146 | ||
|
|
c8ac9c40bb | ||
|
|
c4337fc3e7 | ||
|
|
686a630fbd | ||
|
|
5a227d23d2 | ||
|
|
c7900f4a16 | ||
|
|
312a1c53f3 | ||
|
|
ca456e5236 | ||
|
|
3dc7336b5f | ||
|
|
224f5247b0 | ||
|
|
c6a080f7ad | ||
|
|
b3c877f66d | ||
|
|
419661013b | ||
|
|
9b08c209f9 | ||
|
|
ab07a0bc90 | ||
|
|
91d9a3ef5d | ||
|
|
4c9a42062c | ||
|
|
30be097e15 | ||
|
|
97ae82b2a2 | ||
|
|
aea0ccfa00 | ||
|
|
e934b61856 | ||
|
|
99e96502b9 | ||
|
|
848f2b2396 | ||
|
|
a1e3890681 | ||
|
|
21e2aabc39 | ||
|
|
0a7490ed53 | ||
|
|
1c2c119461 | ||
|
|
49da3c6c35 | ||
|
|
0b27739768 | ||
|
|
ce32226cb5 | ||
|
|
f8b57999e5 | ||
|
|
52c6c6933b | ||
|
|
4c089440e6 | ||
|
|
94f93a1421 | ||
|
|
b3018fcb76 | ||
|
|
3cfe3af1c8 | ||
|
|
70f154da52 | ||
|
|
8a87698cc2 | ||
|
|
7edd3bdc11 | ||
|
|
98da49ff58 | ||
|
|
81ebd758cb | ||
|
|
f5e0819f50 | ||
|
|
eeb59cab07 | ||
|
|
75a0df2975 | ||
|
|
f7c63ee5d8 | ||
|
|
841a835153 | ||
|
|
a6b6077ecd | ||
|
|
caff2c98a9 | ||
|
|
50fa5d670b | ||
|
|
57809826a4 | ||
|
|
d5ab2c1f31 | ||
|
|
2635dcb457 | ||
|
|
e7541f88fb | ||
|
|
1f65cf6555 | ||
|
|
059f1cb698 | ||
|
|
2409d3223a | ||
|
|
7e1f1e5aad | ||
|
|
4180270f0b | ||
|
|
fe4ffc159a | ||
|
|
4cda1e48ef | ||
|
|
c5da98a5d9 | ||
|
|
3c58054a47 | ||
|
|
2d5d4bec02 | ||
|
|
8fb0da34bb | ||
|
|
b686178f93 | ||
|
|
803f69726b | ||
|
|
7764e71257 | ||
|
|
c60fdc8d84 | ||
|
|
fbc85308a5 | ||
|
|
26eda4775e | ||
|
|
e96908fcbb | ||
|
|
7f91d5cba8 | ||
|
|
561aff641a | ||
|
|
874147449a | ||
|
|
6706a0037e | ||
|
|
6d62389d26 |
@@ -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:
|
||||
|
||||
@@ -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.6
|
||||
uses: peter-evans/create-pull-request@v7.0.8
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
commit-message: 'docs(appium): Update documentation translations'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -204,11 +204,6 @@ contribution of project development and leadership!
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://saucelabs.com/resources/blog/appium-strategic-partner"><img src="packages/appium/docs/overrides/assets/images/sponsor-logo-sauce.png"
|
||||
width="300" alt="Sauce Labs" /></a>
|
||||
</p>
|
||||
|
||||
### Other Sponsors
|
||||
|
||||
A full list of sponsors is available at our [Sponsors page](https://appium.io/docs/en/latest/sponsors/).
|
||||
|
||||
Generated
+3717
-3251
File diff suppressed because it is too large
Load Diff
+44
-21
@@ -85,42 +85,65 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@colors/colors": "1.6.0",
|
||||
"@eslint/js": "9.17.0",
|
||||
"@tsconfig/node14": "14.1.2",
|
||||
"@types/chai": "5.0.1",
|
||||
"@types/chai-as-promised": "8.0.1",
|
||||
"@types/diff": "6.0.0",
|
||||
"@eslint/js": "9.22.0",
|
||||
"@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.2.0",
|
||||
"@types/chai-as-promised": "8.0.2",
|
||||
"@types/diff": "7.0.1",
|
||||
"@types/express": "5.0.0",
|
||||
"@types/jsftp": "2.1.5",
|
||||
"@types/json-schema": "7.0.15",
|
||||
"@types/klaw": "3.0.6",
|
||||
"@types/lockfile": "1.0.4",
|
||||
"@types/lodash": "4.17.16",
|
||||
"@types/method-override": "3.0.0",
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/node": "22.10.5",
|
||||
"@types/mv": "2.1.4",
|
||||
"@types/ncp": "2.0.8",
|
||||
"@types/node": "22.13.10",
|
||||
"@types/pluralize": "0.0.33",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/sinon": "17.0.3",
|
||||
"@types/serve-favicon": "2.5.7",
|
||||
"@types/shell-quote": "1.7.5",
|
||||
"@types/sinon": "17.0.4",
|
||||
"@types/sinon-chai": "4.0.0",
|
||||
"@types/source-map-support": "0.5.10",
|
||||
"@types/supports-color": "8.1.3",
|
||||
"@types/teen_process": "2.0.4",
|
||||
"@types/ws": "8.5.13",
|
||||
"@types/uuid": "10.0.0",
|
||||
"@types/which": "3.0.4",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"@types/ws": "8.18.0",
|
||||
"@types/xmldom": "0.1.34",
|
||||
"@types/yargs": "17.0.33",
|
||||
"asyncbox": "3.0.0",
|
||||
"chai": "5.1.2",
|
||||
"chai": "5.2.0",
|
||||
"chai-as-promised": "8.0.1",
|
||||
"conventional-changelog-conventionalcommits": "7.0.2",
|
||||
"cpy-cli": "5.0.0",
|
||||
"eslint": "9.17.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-import-resolver-typescript": "3.7.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "9.22.0",
|
||||
"eslint-config-prettier": "10.1.1",
|
||||
"eslint-import-resolver-typescript": "3.8.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.3",
|
||||
"lerna": "8.1.9",
|
||||
"json-schema-to-typescript": "15.0.4",
|
||||
"lerna": "8.2.1",
|
||||
"log-symbols": "4.1.0",
|
||||
"midnight-smoker": "8.0.0",
|
||||
"mjpeg-consumer": "2.0.0",
|
||||
"mjpeg-server": "0.3.1",
|
||||
"mocha": "11.0.1",
|
||||
"mocha": "11.1.0",
|
||||
"npm-run-all2": "6.2.6",
|
||||
"prettier": "3.4.2",
|
||||
"prettier": "3.5.3",
|
||||
"rewiremock": "3.14.5",
|
||||
"rimraf": "5.0.10",
|
||||
"serve-static": "1.16.2",
|
||||
@@ -128,11 +151,11 @@
|
||||
"sync-monorepo-packages": "1.0.2",
|
||||
"ts-node": "10.9.2",
|
||||
"tsd": "0.31.2",
|
||||
"typescript": "5.7.2",
|
||||
"typescript-eslint": "8.19.0",
|
||||
"typescript": "5.8.2",
|
||||
"typescript-eslint": "8.26.1",
|
||||
"validate.js": "0.13.1",
|
||||
"webdriverio": "9.5.1",
|
||||
"ws": "8.18.0",
|
||||
"webdriverio": "9.10.1",
|
||||
"ws": "8.18.1",
|
||||
"yaml-js": "0.3.1"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -3,6 +3,66 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.1](https://github.com/appium/appium/compare/appium@2.16.0...appium@2.16.1) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package appium
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.0](https://github.com/appium/appium/compare/appium@2.15.0...appium@2.16.0) (2025-02-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add /appium/extensions API to list available extensions ([#20931](https://github.com/appium/appium/issues/20931)) ([a6b6077](https://github.com/appium/appium/commit/a6b6077ecd0749598f52d9f29b3220f47d7ad636))
|
||||
* add /appium/sessions, /session/:sessionId/appium/capabilities and deprecated marks will be removed in the future ([#20936](https://github.com/appium/appium/issues/20936)) ([eeb59ca](https://github.com/appium/appium/commit/eeb59cab071fdafa44f091e9d0e2676414c85c5d))
|
||||
* Add BiDi commands to the listCommands API output ([#20925](https://github.com/appium/appium/issues/20925)) ([2635dcb](https://github.com/appium/appium/commit/2635dcb457be2dc02dfbee5ad4c6ab132f5af8de))
|
||||
* **appium:** Add a command line parameter to configure HTTP server request timeout ([#21003](https://github.com/appium/appium/issues/21003)) ([eb1b156](https://github.com/appium/appium/commit/eb1b156146bc338da9c6ded5a2c5beab22ac0ed8))
|
||||
* **base-driver:** Add an API to list commands ([#20914](https://github.com/appium/appium/issues/20914)) ([059f1cb](https://github.com/appium/appium/commit/059f1cb698ccdbc58494af9303c5bf264a1893d9))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **appium:** add await for async function ([#20974](https://github.com/appium/appium/issues/20974)) ([21e2aab](https://github.com/appium/appium/commit/21e2aabc391fefe0dff8e29fe1cd6be908d5b3d0))
|
||||
* **appium:** Wait for upstream web socket to open before sending data to it ([#20953](https://github.com/appium/appium/issues/20953)) ([3cfe3af](https://github.com/appium/appium/commit/3cfe3af1c8d88c8ec79d2d89c7e76d697205c86b))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.5.3 ([#20895](https://github.com/appium/appium/issues/20895)) ([8741474](https://github.com/appium/appium/commit/874147449a26bbec03b08e7e95fb9e4c6b5881af))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.5.4 ([#20898](https://github.com/appium/appium/issues/20898)) ([7f91d5c](https://github.com/appium/appium/commit/7f91d5cba850100124088d774ba8704e3765863f))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.5.7 ([#20903](https://github.com/appium/appium/issues/20903)) ([c60fdc8](https://github.com/appium/appium/commit/c60fdc8d847b4aeed2d8b8307180194ee32e5ca9))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.7.0 ([#20930](https://github.com/appium/appium/issues/20930)) ([5780982](https://github.com/appium/appium/commit/57809826a410b71dca155f59e081f77dcaf671ed))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.7.1 ([#20934](https://github.com/appium/appium/issues/20934)) ([50fa5d6](https://github.com/appium/appium/commit/50fa5d670bd967200a78e5b4b8a54d5c9b85901b))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.7.2 ([#20944](https://github.com/appium/appium/issues/20944)) ([f5e0819](https://github.com/appium/appium/commit/f5e0819f505b55421b7d260f69a41ae980390a91))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.8.0 ([#20964](https://github.com/appium/appium/issues/20964)) ([ce32226](https://github.com/appium/appium/commit/ce32226cb57fb3f07bf4862dbbfcba227ff1230c))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.9.0 ([#20994](https://github.com/appium/appium/issues/20994)) ([b3c877f](https://github.com/appium/appium/commit/b3c877f66ddf3ccf7a25ca1f7a3952c660bd7dda))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.9.1 ([#20996](https://github.com/appium/appium/issues/20996)) ([c6a080f](https://github.com/appium/appium/commit/c6a080f7ade03a49a274ce6fbc84629406fdc2a7))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.9.3 ([#21005](https://github.com/appium/appium/issues/21005)) ([686a630](https://github.com/appium/appium/commit/686a630fbdf525db16b42fdfc1bbdaf6426b41d0))
|
||||
* **support:** update dependency semver to v7.7.0 ([#20948](https://github.com/appium/appium/issues/20948)) ([81ebd75](https://github.com/appium/appium/commit/81ebd758cb8a87d388124036e5158207041a6b2f))
|
||||
* **support:** update dependency semver to v7.7.1 ([#20961](https://github.com/appium/appium/issues/20961)) ([4c08944](https://github.com/appium/appium/commit/4c089440e655fbe3c8da919f8f625b7d7179f00a))
|
||||
* **types:** update dependency type-fest to v4.32.0 ([#20900](https://github.com/appium/appium/issues/20900)) ([fbc8530](https://github.com/appium/appium/commit/fbc85308a5398e7c9966792da713e60e47ed7f00))
|
||||
* **types:** update dependency type-fest to v4.33.0 ([#20923](https://github.com/appium/appium/issues/20923)) ([2409d32](https://github.com/appium/appium/commit/2409d3223a77aa7e84e0cb05a70be3bfa0c69157))
|
||||
* **types:** update dependency type-fest to v4.34.1 ([#20971](https://github.com/appium/appium/issues/20971)) ([0a7490e](https://github.com/appium/appium/commit/0a7490ed53ccfa9243df779e74bafadfc8415c87))
|
||||
* **types:** update dependency type-fest to v4.35.0 ([#20999](https://github.com/appium/appium/issues/20999)) ([3dc7336](https://github.com/appium/appium/commit/3dc7336b5fce10b9c1b095cd7a8a1841dbc3de12))
|
||||
|
||||
|
||||
|
||||
## [2.15.0](https://github.com/appium/appium/compare/appium@2.14.1...appium@2.15.0) (2025-01-08)
|
||||
|
||||
|
||||
|
||||
@@ -204,11 +204,6 @@ contribution of project development and leadership!
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://saucelabs.com/resources/blog/appium-strategic-partner"><img src="packages/appium/docs/overrides/assets/images/sponsor-logo-sauce.png"
|
||||
width="300" alt="Sauce Labs" /></a>
|
||||
</p>
|
||||
|
||||
### Other Sponsors
|
||||
|
||||
A full list of sponsors is available at our [Sponsors page](https://appium.io/docs/en/latest/sponsors/).
|
||||
|
||||
@@ -34,6 +34,7 @@ below.
|
||||
|`--driver`|Driver-specific configuration. Keys should correspond to driver package names|object|||
|
||||
|`--drivers-import-chunk-size`|The maximum amount of drivers that could be imported in parallel on server startup|number|`3`||
|
||||
|`--keep-alive-timeout`|Number of seconds the Appium server should apply as both the keep-alive timeout and the connection timeout for all requests. Setting this to `0` disables the timeout.|integer|`600`|`-ka`|
|
||||
|`--request-timeout`|Number of seconds the Appium server should apply for receiving the entire HTTP request from the client. A value of 0 disables the timeout. Set it to a non-zero value to protect against potential Denial-of-Service attacks in case the server is deployed without a reverse proxy in front. HTTP requests that are running longer than allowed by this timeout would be rejected with the status code 408.|integer|`3600`||
|
||||
|`--local-timezone`|Use local timezone for timestamps|boolean|`false`||
|
||||
|`--log`|Also send log output to this file|string||`-g`|
|
||||
|`--log-filters`|One or more log filtering rules|array|||
|
||||
|
||||
@@ -47,7 +47,13 @@ A session data object
|
||||
|
||||
`GET` **`/session/:sessionId`**
|
||||
|
||||
Returns capabilities for the session and event history (if applicable)
|
||||
Returns capabilities for the session and event history (if applicable).
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
Please use `getAppiumSessionCapabilities` to get the session capabilities.
|
||||
Please use `getLogEvents` to get event history.
|
||||
|
||||
|
||||
<!-- comment source: multiple -->
|
||||
|
||||
@@ -57,6 +63,20 @@ Returns capabilities for the session and event history (if applicable)
|
||||
|
||||
A session data object
|
||||
|
||||
### `getAppiumSessionCapabilities`
|
||||
|
||||
`GET` **`/session/:sessionId/appium/capabilities`**
|
||||
|
||||
Returns capabilities for the session.
|
||||
|
||||
<!-- comment source: multiple -->
|
||||
|
||||
#### Response
|
||||
|
||||
`AppiumSessionCapabilities`
|
||||
|
||||
A session data object
|
||||
|
||||
### `findElement`
|
||||
|
||||
`POST` **`/session/:sessionId/element`**
|
||||
|
||||
@@ -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`
|
||||
@@ -32,6 +32,7 @@ These clients are not maintained by the Appium team and can be used with other l
|
||||
|[Nightwatch.js](https://nightwatchjs.org/guide/mobile-app-testing/introduction.html)|Node.js|
|
||||
|[RobotFramework](https://github.com/serhatbolsu/robotframework-appiumlibrary)|DSL|
|
||||
|[multicatch's appium-client](https://github.com/multicatch/appium-client)|Rust|
|
||||
|[SwiftAppium](https://github.com/milcgroup/swiftappium)|Swift|
|
||||
|
||||
In general, any W3C WebDriver spec-compatible client will also integrate well with Appium, though
|
||||
some Appium-specific commands may not be implemented in other clients.
|
||||
|
||||
@@ -44,8 +44,6 @@ 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|
|
||||
|[axe UiAutomator2](https://docs.deque.com/devtools-mobile/appium-setup)|`--source=npm @axe-devtools/axe-appium-uiautomator2-driver`|Android|Native|[Deque](https://www.deque.com)|
|
||||
|[axe XCUITest](https://docs.deque.com/devtools-mobile/appium-setup)|`--source=npm @axe-devtools/axe-appium-xcuitest-driver`|iOS|Native|[Deque](https://www.deque.com)|
|
||||
|
||||
!!! 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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ information and command length. This is an advanced feature that is controlled
|
||||
by the use of the `appium:eventTimings` capability (set it to `true` to log event
|
||||
timings).
|
||||
|
||||
With this capability turned on, the `GET /session/:id` response (i.e., the
|
||||
response to `driver.getSessionDetails()` or similar, depending on client) will
|
||||
be decorated with an `events` property. This is the structure of that `events`
|
||||
With this capability turned on, the `POST /session/:id/appium/events` response (i.e.,
|
||||
the response to `driver.logs.events` or similar, depending on client) will be
|
||||
decorated with an `events` property. This is the structure of that `events`
|
||||
property:
|
||||
|
||||
```
|
||||
@@ -54,13 +54,17 @@ of events, or statistical information about average length of a certain type of
|
||||
command, and so on.
|
||||
|
||||
You can only receive data about events that have happened when you make the
|
||||
call to `/session/:id`, so the best time to get data about an entire session is
|
||||
call to `/session/:id/appium/events`, so the best time to get data about an entire session is
|
||||
right before quitting it.
|
||||
|
||||
The Appium team maintains an event timings parser tool that can be used to
|
||||
generate various kinds of reports from event timings output:
|
||||
[appium/appium-event-parser](https://github.com/appium/appium-event-parser).
|
||||
|
||||
!!! note
|
||||
|
||||
In the past, events were available as a part of `GET /session/:id` response
|
||||
|
||||
## Add a custom event
|
||||
|
||||
!!! warning "TODO"
|
||||
|
||||
@@ -40,11 +40,6 @@ Android TV, Samsung), and more!
|
||||
<img src="assets/images/sponsor-logo-browserstack-light.png#only-light" style="width: 220px;" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="homepageSponsor">
|
||||
<a href="https://saucelabs.com/resources/blog/appium-strategic-partner">
|
||||
<img src="assets/images/sponsor-logo-sauce.png" style="width: 200px;" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ contributors!
|
||||
<img src="../assets/images/sponsor-logo-browserstack-dark.png" width="250" alt="Browserstack"/>
|
||||
</picture>
|
||||
</a>
|
||||
<a style="padding-left: 1em" href="https://saucelabs.com/resources/blog/appium-strategic-partner" target="_blank"><img src="../assets/images/sponsor-logo-sauce.png" width="250"/></a>
|
||||
|
||||
## Gold Sponsors
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -4,8 +4,4 @@
|
||||
<a href="https://www.browserstack.com/browserstack-appium?utm_campaigncode=701OW00000AoUTQYA3&utm_medium=partnered&utm_source=appium">
|
||||
<img src="/docs/en/latest/assets/images/sponsor-logo-browserstack-dark.png" />
|
||||
</a>
|
||||
|
||||
<a href="https://saucelabs.com/resources/blog/appium-strategic-partner">
|
||||
<img src="/docs/en/latest/assets/images/sponsor-logo-sauce.png" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -10,9 +10,9 @@ information and command length. This is an advanced feature that is controlled
|
||||
by the use of the `appium:eventTimings` capability (set it to `true` to log event
|
||||
timings).
|
||||
|
||||
With this capability turned on, the `GET /session/:id` response (i.e., the
|
||||
response to `driver.getSessionDetails()` or similar, depending on client) will
|
||||
be decorated with an `events` property. This is the structure of that `events`
|
||||
With this capability turned on, the `POST /session/:id/appium/events` response (i.e.,
|
||||
the response to `driver.logs.events` or similar, depending on client) will be
|
||||
decorated with an `events` property. This is the structure of that `events`
|
||||
property:
|
||||
|
||||
```
|
||||
@@ -54,13 +54,17 @@ of events, or statistical information about average length of a certain type of
|
||||
command, and so on.
|
||||
|
||||
You can only receive data about events that have happened when you make the
|
||||
call to `/session/:id`, so the best time to get data about an entire session is
|
||||
call to `/session/:id/appium/events`, so the best time to get data about an entire session is
|
||||
right before quitting it.
|
||||
|
||||
The Appium team maintains an event timings parser tool that can be used to
|
||||
generate various kinds of reports from event timings output:
|
||||
[appium/appium-event-parser](https://github.com/appium/appium-event-parser).
|
||||
|
||||
!!! note
|
||||
|
||||
In the past, events were available as a part of `GET /session/:id` response
|
||||
|
||||
## Add a custom event
|
||||
|
||||
!!! warning "TODO"
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
CREATE_SESSION_COMMAND,
|
||||
DELETE_SESSION_COMMAND,
|
||||
GET_STATUS_COMMAND,
|
||||
LIST_DRIVER_COMMANDS_COMMAND,
|
||||
LIST_DRIVER_EXTENSIONS_COMMAND,
|
||||
promoteAppiumOptions,
|
||||
promoteAppiumOptionsForObject,
|
||||
generateDriverLogPrefix,
|
||||
@@ -22,7 +24,8 @@ import {
|
||||
import {util} from '@appium/support';
|
||||
import {getDefaultsForExtension} from './schema';
|
||||
import {DRIVER_TYPE, BIDI_BASE_PATH} from './constants';
|
||||
import * as bidiHelpers from './bidi';
|
||||
import * as bidiCommands from './bidi-commands';
|
||||
import * as inspectorCommands from './inspector-commands';
|
||||
|
||||
const desiredCapabilityConstraints = /** @type {const} */ ({
|
||||
automationName: {
|
||||
@@ -192,6 +195,11 @@ class AppiumDriver extends DriverCore {
|
||||
}));
|
||||
}
|
||||
|
||||
async getAppiumSessions () {
|
||||
throw new errors.NotImplementedError('Not implemented yet. ' +
|
||||
'Please check https://github.com/appium/appium/issues/20880 for more details.');
|
||||
}
|
||||
|
||||
printNewSessionAnnouncement(driverName, driverVersion, driverBaseVersion) {
|
||||
this.log.info(
|
||||
driverVersion
|
||||
@@ -403,7 +411,7 @@ class AppiumDriver extends DriverCore {
|
||||
);
|
||||
|
||||
// set the New Command Timeout for the inner driver
|
||||
driverInstance.startNewCommandTimeout();
|
||||
await driverInstance.startNewCommandTimeout();
|
||||
|
||||
// apply initial values to Appium settings (if provided)
|
||||
if (driverInstance.isW3CProtocol() && !_.isEmpty(w3cSettings)) {
|
||||
@@ -426,7 +434,7 @@ class AppiumDriver extends DriverCore {
|
||||
if (dCaps.webSocketUrl) {
|
||||
const {address, port, basePath} = this.args;
|
||||
const scheme = `ws${this.server.isSecure() ? 's' : ''}`;
|
||||
const host = bidiHelpers.determineBiDiHost(address);
|
||||
const host = bidiCommands.determineBiDiHost(address);
|
||||
const bidiUrl = `${scheme}://${host}:${port}${basePath}${BIDI_BASE_PATH}/${innerSessionId}`;
|
||||
this.log.info(
|
||||
`Upstream driver responded with webSocketUrl ${dCaps.webSocketUrl}, will rewrite to ` +
|
||||
@@ -565,32 +573,6 @@ class AppiumDriver extends DriverCore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} sessionId
|
||||
*/
|
||||
cleanupBidiSockets(sessionId) {
|
||||
// clean up any bidi sockets associated with session
|
||||
if (this.bidiSockets[sessionId]) {
|
||||
try {
|
||||
this.log.debug(`Closing bidi socket(s) associated with session ${sessionId}`);
|
||||
for (const ws of this.bidiSockets[sessionId]) {
|
||||
// 1001 means server is going away
|
||||
ws.close(1001, 'Appium session is closing');
|
||||
}
|
||||
} catch {}
|
||||
delete this.bidiSockets[sessionId];
|
||||
const proxyClient = this.bidiProxyClients[sessionId];
|
||||
if (proxyClient) {
|
||||
this.log.debug(`Also closing proxy connection to upstream bidi server`);
|
||||
try {
|
||||
// 1000 means normal closure, which seems correct when Appium is acting as the client
|
||||
proxyClient.close(1000);
|
||||
} catch {}
|
||||
delete this.bidiProxyClients[sessionId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAllSessions(opts = {}) {
|
||||
const sessionsCount = _.size(this.sessions);
|
||||
if (0 === sessionsCount) {
|
||||
@@ -935,15 +917,28 @@ class AppiumDriver extends DriverCore {
|
||||
return dstSession && dstSession.canProxy(sessionId);
|
||||
}
|
||||
|
||||
onBidiConnection = bidiHelpers.onBidiConnection;
|
||||
onBidiMessage = bidiHelpers.onBidiMessage;
|
||||
onBidiServerError = bidiHelpers.onBidiServerError;
|
||||
onBidiConnection = bidiCommands.onBidiConnection;
|
||||
onBidiMessage = bidiCommands.onBidiMessage;
|
||||
onBidiServerError = bidiCommands.onBidiServerError;
|
||||
cleanupBidiSockets = bidiCommands.cleanupBidiSockets;
|
||||
|
||||
listCommands = inspectorCommands.listCommands;
|
||||
listExtensions = inspectorCommands.listExtensions;
|
||||
}
|
||||
|
||||
// help decide which commands should be proxied to sub-drivers and which
|
||||
// should be handled by this, our umbrella driver
|
||||
/**
|
||||
* Help decide which commands should be proxied to sub-drivers and which
|
||||
* should be handled by this, our umbrella driver
|
||||
* @param {string} cmd
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isAppiumDriverCommand(cmd) {
|
||||
return !isSessionCommand(cmd) || cmd === DELETE_SESSION_COMMAND;
|
||||
return !isSessionCommand(cmd)
|
||||
|| _.includes([
|
||||
DELETE_SESSION_COMMAND,
|
||||
LIST_DRIVER_COMMANDS_COMMAND,
|
||||
LIST_DRIVER_EXTENSIONS_COMMAND,
|
||||
], cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -176,6 +176,38 @@ export function onBidiServerError(this: AppiumDriver, err: Error): void {
|
||||
this.log.warn(`Error from bidi websocket server: ${err}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up any bidi sockets associated with session
|
||||
*
|
||||
* @param sessionId
|
||||
*/
|
||||
export function cleanupBidiSockets(this: AppiumDriver, sessionId: string): void {
|
||||
if (!this.bidiSockets[sessionId]) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.log.debug(`Closing bidi socket(s) associated with session ${sessionId}`);
|
||||
for (const ws of this.bidiSockets[sessionId]) {
|
||||
// 1001 means server is going away
|
||||
ws.close(1001, 'Appium session is closing');
|
||||
}
|
||||
} catch {}
|
||||
delete this.bidiSockets[sessionId];
|
||||
|
||||
const proxyClient = this.bidiProxyClients[sessionId];
|
||||
if (!proxyClient) {
|
||||
return;
|
||||
}
|
||||
this.log.debug(`Also closing proxy connection to upstream bidi server`);
|
||||
try {
|
||||
// 1000 means normal closure, which seems correct when Appium is acting as the client
|
||||
proxyClient.close(1000);
|
||||
} catch {}
|
||||
delete this.bidiProxyClients[sessionId];
|
||||
}
|
||||
|
||||
// #region Private functions
|
||||
|
||||
/**
|
||||
* Initialize a new bidi connection
|
||||
* @param ws The websocket connection object
|
||||
@@ -263,6 +295,7 @@ function initBidiSocket(this: AppiumDriver, ws: WebSocket, req: IncomingMessage)
|
||||
const socketSend = B.promisify(socket.send, {context: socket});
|
||||
return async (data: string | Buffer) => {
|
||||
try {
|
||||
await assertIsOpen(socket);
|
||||
await socketSend(data);
|
||||
} catch (err) {
|
||||
logSocketErr(err);
|
||||
@@ -468,3 +501,43 @@ function initBidiEventListeners(
|
||||
plugin.eventEmitter?.on(BIDI_EVENT_NAME, eventListenerFactory('plugin', plugin));
|
||||
}
|
||||
}
|
||||
|
||||
async function assertIsOpen(
|
||||
ws: WebSocket,
|
||||
timeoutMs: number = 5000,
|
||||
): Promise<WebSocket> {
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
return ws;
|
||||
}
|
||||
if (ws.readyState > ws.OPEN) {
|
||||
throw new Error(`The BiDi web socket at ${ws.url} is not open`);
|
||||
}
|
||||
|
||||
let errorListener;
|
||||
let openListener;
|
||||
// The socket is in CONNECTING state. Wait up to `timeoutMs` until it is open
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
setTimeout(() => reject(
|
||||
new Error(
|
||||
`The BiDi web socket at ${ws.url} did not ` +
|
||||
`open after ${timeoutMs}ms timeout`
|
||||
)
|
||||
), timeoutMs);
|
||||
ws.once('error', reject);
|
||||
errorListener = reject;
|
||||
ws.once('open', resolve);
|
||||
openListener = resolve;
|
||||
});
|
||||
} finally {
|
||||
if (errorListener) {
|
||||
ws.off('error', errorListener);
|
||||
}
|
||||
if (openListener) {
|
||||
ws.off('open', openListener);
|
||||
}
|
||||
}
|
||||
return ws;
|
||||
}
|
||||
|
||||
// #endregion
|
||||
@@ -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',
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
METHOD_MAP as BASE_METHOD_MAP,
|
||||
BIDI_COMMANDS as BASE_BIDI_COMMANDS,
|
||||
} from '@appium/base-driver';
|
||||
import type {
|
||||
ListCommandsResponse,
|
||||
MethodMap,
|
||||
BidiModuleMap,
|
||||
DriverClass,
|
||||
PluginClass,
|
||||
ListExtensionsResponse,
|
||||
PayloadParams,
|
||||
RestCommandItemParam,
|
||||
RestMethodsToCommandsMap,
|
||||
BiDiCommandsMap,
|
||||
BidiMethodParams,
|
||||
BiDiCommandItemParam,
|
||||
BiDiCommandNamesToInfosMap,
|
||||
ExecuteMethodMap,
|
||||
} from '@appium/types';
|
||||
import type { AppiumDriver } from './appium';
|
||||
|
||||
|
||||
export async function listCommands(this: AppiumDriver, sessionId?: string): Promise<ListCommandsResponse> {
|
||||
let driverRestMethodMap: MethodMap<any> = {};
|
||||
let driverBiDiCommands: BidiModuleMap = {};
|
||||
let pluginRestMethodMaps: Record<string, MethodMap<any>> = {};
|
||||
let pluginBiDiCommands: Record<string, BidiModuleMap> = {};
|
||||
if (sessionId) {
|
||||
const driverClass = this.driverForSession(sessionId)?.constructor as (DriverClass | undefined);
|
||||
driverRestMethodMap = driverClass?.newMethodMap ?? {};
|
||||
driverBiDiCommands = driverClass?.newBidiCommands ?? {};
|
||||
const pluginClasses = this.pluginsForSession(sessionId)
|
||||
.map((p) => p.constructor as PluginClass);
|
||||
pluginRestMethodMaps = _.fromPairs(pluginClasses.map((c) => [c.name, c.newMethodMap ?? {}]));
|
||||
pluginBiDiCommands = _.fromPairs(pluginClasses.map((c) => [c.name, c.newBidiCommands ?? {}]));
|
||||
}
|
||||
return {
|
||||
rest: {
|
||||
base: methodMapToRestCommandsInfo(BASE_METHOD_MAP),
|
||||
driver: methodMapToRestCommandsInfo(driverRestMethodMap),
|
||||
plugins: pluginRestMethodMaps ? _.mapValues(pluginRestMethodMaps, methodMapToRestCommandsInfo) : undefined,
|
||||
},
|
||||
bidi: toBiDiCommandsMap(BASE_BIDI_COMMANDS, driverBiDiCommands, pluginBiDiCommands),
|
||||
};
|
||||
}
|
||||
|
||||
export async function listExtensions(this: AppiumDriver, sessionId?: string): Promise<ListExtensionsResponse> {
|
||||
let driverExecuteMethodMap: ExecuteMethodMap<any> = {};
|
||||
let pluginExecuteMethodMaps: Record<string, ExecuteMethodMap<any>> = {};
|
||||
if (sessionId) {
|
||||
const driverClass = this.driverForSession(sessionId)?.constructor as (DriverClass | undefined);
|
||||
driverExecuteMethodMap = driverClass?.executeMethodMap ?? {};
|
||||
const pluginClasses = this.pluginsForSession(sessionId)
|
||||
.map((p) => p.constructor as PluginClass);
|
||||
pluginExecuteMethodMaps = _.fromPairs(pluginClasses.map((c) => [c.name, c.executeMethodMap ?? {}]));
|
||||
}
|
||||
return {
|
||||
rest: {
|
||||
driver: executeMethodMapToCommandsInfo(driverExecuteMethodMap),
|
||||
plugins: pluginExecuteMethodMaps ? _.mapValues(pluginExecuteMethodMaps, executeMethodMapToCommandsInfo) : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function toRestCommandParams(params: PayloadParams | undefined): RestCommandItemParam[] | undefined {
|
||||
if (!params) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toRestCommandItemParam = (x: any, isRequired: boolean): RestCommandItemParam | undefined => {
|
||||
const isNameAnArray = _.isArray(x);
|
||||
const name = isNameAnArray ? x[0] : x;
|
||||
if (!_.isString(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If parameter names are arrays then this means
|
||||
// either of them is required.
|
||||
// Not sure we could reflect that in here.
|
||||
const required = isRequired && !isNameAnArray;
|
||||
return {
|
||||
name,
|
||||
required,
|
||||
};
|
||||
};
|
||||
|
||||
const requiredParams: RestCommandItemParam[] = (params.required ?? [])
|
||||
.map((name: any) => toRestCommandItemParam(name, true))
|
||||
.filter((x) => !_.isUndefined(x));
|
||||
const optionalParams: RestCommandItemParam[] = (params.optional ?? [])
|
||||
.map((name: any) => toRestCommandItemParam(name, false))
|
||||
.filter((x) => !_.isUndefined(x));
|
||||
return requiredParams.length || optionalParams.length
|
||||
? [...requiredParams, ...optionalParams]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function methodMapToRestCommandsInfo (mm: MethodMap<any>): Record<string, RestMethodsToCommandsMap> {
|
||||
const res: Record<string, RestMethodsToCommandsMap> = {};
|
||||
for (const [uriPath, methods] of _.toPairs(mm)) {
|
||||
const methodsMap = {};
|
||||
for (const [method, spec] of _.toPairs(methods)) {
|
||||
methodsMap[method] = {
|
||||
command: spec.command,
|
||||
deprecated: spec.deprecated,
|
||||
info: spec.info,
|
||||
params: toRestCommandParams(spec.payloadParams),
|
||||
};
|
||||
}
|
||||
res[uriPath] = methodsMap;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function executeMethodMapToCommandsInfo(emm: ExecuteMethodMap<any>): RestMethodsToCommandsMap {
|
||||
const result: RestMethodsToCommandsMap = {};
|
||||
for (const [name, info] of _.toPairs(emm)) {
|
||||
result[name] = {
|
||||
command: info.command,
|
||||
deprecated: info.deprecated,
|
||||
info: info.info,
|
||||
params: toRestCommandParams(info.params),
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function toBiDiCommandsMap(
|
||||
baseModuleMap: BidiModuleMap,
|
||||
driverModuleMap: BidiModuleMap,
|
||||
pluginModuleMaps: Record<string, BidiModuleMap>
|
||||
): BiDiCommandsMap {
|
||||
const toBiDiCommandParams = (params: BidiMethodParams | undefined): BiDiCommandItemParam[] | undefined => {
|
||||
if (!params) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toBiDiCommandItemParam = (x: any, isRequired: boolean): BiDiCommandItemParam | undefined => {
|
||||
const isNameAnArray = _.isArray(x);
|
||||
const name = isNameAnArray ? x[0] : x;
|
||||
if (!_.isString(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If parameter names are arrays then this means
|
||||
// either of them is required.
|
||||
// Not sure we could reflect that in here.
|
||||
const required = isRequired && !isNameAnArray;
|
||||
return {
|
||||
name,
|
||||
required,
|
||||
};
|
||||
};
|
||||
|
||||
const requiredParams: BiDiCommandItemParam[] = (params.required ?? [])
|
||||
.map((name) => toBiDiCommandItemParam(name, true))
|
||||
.filter((x) => !_.isUndefined(x));
|
||||
const optionalParams: BiDiCommandItemParam[] = (params.optional ?? [])
|
||||
.map((name) => toBiDiCommandItemParam(name, false))
|
||||
.filter((x) => !_.isUndefined(x));
|
||||
return requiredParams.length || optionalParams.length
|
||||
? [...requiredParams, ...optionalParams]
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const moduleMapToBiDiCommandsInfo = (mm: BidiModuleMap): Record<string, BiDiCommandNamesToInfosMap> => {
|
||||
const res: Record<string, BiDiCommandNamesToInfosMap> = {};
|
||||
for (const [domain, commands] of _.toPairs(mm)) {
|
||||
const commandsMap = {};
|
||||
for (const [name, spec] of _.toPairs(commands)) {
|
||||
commandsMap[name] = {
|
||||
command: spec.command,
|
||||
deprecated: spec.deprecated,
|
||||
info: spec.info,
|
||||
params: toBiDiCommandParams(spec.params),
|
||||
};
|
||||
}
|
||||
res[domain] = commandsMap;
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
return {
|
||||
base: moduleMapToBiDiCommandsInfo(baseModuleMap),
|
||||
driver: moduleMapToBiDiCommandsInfo(driverModuleMap),
|
||||
plugins: pluginModuleMaps ? _.mapValues(pluginModuleMaps, moduleMapToBiDiCommandsInfo) : undefined,
|
||||
};
|
||||
}
|
||||
@@ -397,8 +397,10 @@ async function main(args) {
|
||||
extraMethodMap,
|
||||
cliArgs: parsedArgs,
|
||||
};
|
||||
if (parsedArgs.keepAliveTimeout) {
|
||||
serverOpts.keepAliveTimeout = parsedArgs.keepAliveTimeout * 1000;
|
||||
for (const timeoutArgName of ['keepAliveTimeout', 'requestTimeout']) {
|
||||
if (_.isInteger(parsedArgs[timeoutArgName])) {
|
||||
serverOpts[timeoutArgName] = parsedArgs[timeoutArgName] * 1000;
|
||||
}
|
||||
}
|
||||
let server;
|
||||
const bidiServer = new WebSocketServer({noServer: true});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "appium",
|
||||
"version": "2.15.0",
|
||||
"version": "2.17.0",
|
||||
"description": "Automation for Apps.",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -60,40 +60,34 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-driver": "^9.15.0",
|
||||
"@appium/base-plugin": "^2.3.0",
|
||||
"@appium/docutils": "^1.0.29",
|
||||
"@appium/base-driver": "^9.16.3",
|
||||
"@appium/base-plugin": "^2.3.4",
|
||||
"@appium/docutils": "^1.0.33",
|
||||
"@appium/logger": "^1.6.1",
|
||||
"@appium/schema": "^0.7.1",
|
||||
"@appium/support": "^6.0.3",
|
||||
"@appium/types": "^0.24.0",
|
||||
"@appium/schema": "^0.8.1",
|
||||
"@appium/support": "^6.0.7",
|
||||
"@appium/types": "^0.25.2",
|
||||
"@sidvind/better-ajv-errors": "3.0.1",
|
||||
"@types/argparse": "2.0.17",
|
||||
"@types/bluebird": "3.5.42",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/teen_process": "2.0.4",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"ajv": "8.17.1",
|
||||
"ajv-formats": "3.0.1",
|
||||
"argparse": "2.0.1",
|
||||
"async-lock": "1.4.1",
|
||||
"asyncbox": "3.0.0",
|
||||
"axios": "1.7.9",
|
||||
"axios": "1.8.2",
|
||||
"bluebird": "3.7.2",
|
||||
"cross-env": "7.0.3",
|
||||
"lilconfig": "3.1.3",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "10.4.3",
|
||||
"ora": "5.4.1",
|
||||
"package-changed": "3.0.0",
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.6.3",
|
||||
"semver": "7.7.1",
|
||||
"source-map-support": "0.5.21",
|
||||
"teen_process": "2.2.3",
|
||||
"type-fest": "4.31.0",
|
||||
"teen_process": "2.3.1",
|
||||
"type-fest": "4.37.0",
|
||||
"winston": "3.17.0",
|
||||
"wrap-ansi": "7.0.0",
|
||||
"ws": "8.18.0",
|
||||
"ws": "8.18.1",
|
||||
"yaml": "2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"webdriverio": "9.5.1"
|
||||
"webdriverio": "9.10.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +208,77 @@ describe('FakeDriver via HTTP', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('inspector commands', function () {
|
||||
withServer();
|
||||
let driver;
|
||||
|
||||
beforeEach(async function () {
|
||||
driver = await wdio({...wdOpts, capabilities: caps});
|
||||
});
|
||||
afterEach(async function () {
|
||||
if (driver) {
|
||||
await driver.deleteSession();
|
||||
driver = null;
|
||||
}
|
||||
});
|
||||
|
||||
it('should list available driver commands', async function () {
|
||||
driver.addCommand(
|
||||
'listCommands',
|
||||
async () => (await axios.get(
|
||||
`${testServerBaseSessionUrl}/${driver.sessionId}/appium/commands`
|
||||
)).data.value
|
||||
);
|
||||
|
||||
const commands = await driver.listCommands();
|
||||
|
||||
JSON.stringify(commands.rest.base['/session/:sessionId/frame'])
|
||||
.should.eql(JSON.stringify({POST: {command: 'setFrame', params: [
|
||||
{name: 'id', required: true}
|
||||
]}}));
|
||||
_.size(commands.rest.driver).should.be.greaterThan(1);
|
||||
|
||||
JSON.stringify(commands.bidi.base.session.subscribe).should.eql(
|
||||
JSON.stringify({
|
||||
command: 'bidiSubscribe',
|
||||
'params': [
|
||||
{
|
||||
name: 'events',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'contexts',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
_.size(commands.bidi.base).should.be.greaterThan(1);
|
||||
_.size(commands.bidi.driver).should.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('should list available driver extensions', async function () {
|
||||
driver.addCommand(
|
||||
'listExtensions',
|
||||
async () => (await axios.get(
|
||||
`${testServerBaseSessionUrl}/${driver.sessionId}/appium/extensions`
|
||||
)).data.value
|
||||
);
|
||||
|
||||
const extensions = await driver.listExtensions();
|
||||
JSON.stringify(extensions.rest.driver['fake: setThing']).should.eql(
|
||||
JSON.stringify({
|
||||
command: 'setFakeThing',
|
||||
params: [{
|
||||
name: 'thing',
|
||||
required: true
|
||||
}]
|
||||
})
|
||||
);
|
||||
_.size(extensions.rest.driver).should.be.greaterThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('session handling', function () {
|
||||
withServer();
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ export default {
|
||||
denyInsecure: [],
|
||||
driversImportChunkSize: 3,
|
||||
keepAliveTimeout: 600,
|
||||
requestTimeout: 3600,
|
||||
localTimezone: false,
|
||||
logFormat: 'text',
|
||||
loglevel: 'debug',
|
||||
|
||||
@@ -3,6 +3,58 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **base-driver:** Restore the legacy proxy url behaviour ([#21021](https://github.com/appium/appium/issues/21021)) ([dd64a48](https://github.com/appium/appium/commit/dd64a48fca047a728ad109d525bc4787daa75edc))
|
||||
|
||||
|
||||
|
||||
## [9.16.1](https://github.com/appium/appium/compare/@appium/base-driver@9.16.0...@appium/base-driver@9.16.1) (2025-02-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **base-driver:** Optimize the logic of getUrlForProxy ([#21018](https://github.com/appium/appium/issues/21018)) ([8a664a4](https://github.com/appium/appium/commit/8a664a4eef97f45c5bdf7f19708149488b63d7fc))
|
||||
|
||||
|
||||
|
||||
## [9.16.0](https://github.com/appium/appium/compare/@appium/base-driver@9.15.0...@appium/base-driver@9.16.0) (2025-02-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add /appium/extensions API to list available extensions ([#20931](https://github.com/appium/appium/issues/20931)) ([a6b6077](https://github.com/appium/appium/commit/a6b6077ecd0749598f52d9f29b3220f47d7ad636))
|
||||
* add /appium/sessions, /session/:sessionId/appium/capabilities and deprecated marks will be removed in the future ([#20936](https://github.com/appium/appium/issues/20936)) ([eeb59ca](https://github.com/appium/appium/commit/eeb59cab071fdafa44f091e9d0e2676414c85c5d))
|
||||
* Add BiDi commands to the listCommands API output ([#20925](https://github.com/appium/appium/issues/20925)) ([2635dcb](https://github.com/appium/appium/commit/2635dcb457be2dc02dfbee5ad4c6ab132f5af8de))
|
||||
* **appium:** Add a command line parameter to configure HTTP server request timeout ([#21003](https://github.com/appium/appium/issues/21003)) ([eb1b156](https://github.com/appium/appium/commit/eb1b156146bc338da9c6ded5a2c5beab22ac0ed8))
|
||||
* **base-driver:** Add an API to list commands ([#20914](https://github.com/appium/appium/issues/20914)) ([059f1cb](https://github.com/appium/appium/commit/059f1cb698ccdbc58494af9303c5bf264a1893d9))
|
||||
* **base-driver:** Print the closest match if the given script did not match ([#20956](https://github.com/appium/appium/issues/20956)) ([f8b5799](https://github.com/appium/appium/commit/f8b57999e5a4a13a089cff12954de5d99c8a35fc))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **driver-test-support:** update definitelytyped ([#20942](https://github.com/appium/appium/issues/20942)) ([f7c63ee](https://github.com/appium/appium/commit/f7c63ee5d87be21eba577a4cfd0b0a08a050afeb))
|
||||
* **types:** update dependency type-fest to v4.32.0 ([#20900](https://github.com/appium/appium/issues/20900)) ([fbc8530](https://github.com/appium/appium/commit/fbc85308a5398e7c9966792da713e60e47ed7f00))
|
||||
* **types:** update dependency type-fest to v4.33.0 ([#20923](https://github.com/appium/appium/issues/20923)) ([2409d32](https://github.com/appium/appium/commit/2409d3223a77aa7e84e0cb05a70be3bfa0c69157))
|
||||
* **types:** update dependency type-fest to v4.34.1 ([#20971](https://github.com/appium/appium/issues/20971)) ([0a7490e](https://github.com/appium/appium/commit/0a7490ed53ccfa9243df779e74bafadfc8415c87))
|
||||
* **types:** update dependency type-fest to v4.35.0 ([#20999](https://github.com/appium/appium/issues/20999)) ([3dc7336](https://github.com/appium/appium/commit/3dc7336b5fce10b9c1b095cd7a8a1841dbc3de12))
|
||||
|
||||
|
||||
|
||||
## [9.15.0](https://github.com/appium/appium/compare/@appium/base-driver@9.14.1...@appium/base-driver@9.15.0) (2025-01-08)
|
||||
|
||||
|
||||
|
||||
@@ -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?
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '@appium/types';
|
||||
import {mixin} from './mixin';
|
||||
import {BaseDriver} from '../driver';
|
||||
import {distance} from 'fastest-levenshtein';
|
||||
|
||||
declare module '../driver' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -26,9 +27,33 @@ const ExecuteCommands: IExecuteCommands = {
|
||||
const commandMetadata = {...Driver.executeMethodMap?.[script]};
|
||||
if (!commandMetadata.command) {
|
||||
const availableScripts = _.keys(Driver.executeMethodMap);
|
||||
if (_.isEmpty(availableScripts)) {
|
||||
throw new errors.UnsupportedOperationError(
|
||||
`Unsupported execute method '${script}'. ` +
|
||||
`Make sure the installed ${Driver.name} is up-to-date. ` +
|
||||
`The current driver version does not define any execute methods.`
|
||||
);
|
||||
}
|
||||
const matchesMap: StringRecord<string[]> = availableScripts
|
||||
.map((name) => [distance(script, name), name])
|
||||
.reduce((acc, [key, value]) => {
|
||||
if (key in acc) {
|
||||
acc[key].push(value);
|
||||
} else {
|
||||
acc[key] = [value];
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
const sortedMatches = _.flatten(
|
||||
_.keys(matchesMap)
|
||||
.sort((a, b) => parseInt(a, 10) - parseInt(b, 10))
|
||||
.map((x) => matchesMap[x])
|
||||
);
|
||||
throw new errors.UnsupportedOperationError(
|
||||
`Unsupported execute method '${script}'. Available methods ` +
|
||||
`are: ${availableScripts.join(', ')}`
|
||||
`Unsupported execute method '${script}', did you mean '${sortedMatches[0]}'? ` +
|
||||
`Make sure the installed ${Driver.name} is up-to-date. ` +
|
||||
`Execute methods available in the current driver version are: ` +
|
||||
sortedMatches.join(', ')
|
||||
);
|
||||
}
|
||||
const args = validateExecuteMethodParams(protoArgs, commandMetadata.params);
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type InitialOpts,
|
||||
type DefaultDeleteSessionResult,
|
||||
type SingularSessionData,
|
||||
type SessionCapabilities,
|
||||
} from '@appium/types';
|
||||
import B from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
@@ -359,6 +360,8 @@ export class BaseDriver<
|
||||
|
||||
/**
|
||||
* Returns capabilities for the session and event history (if applicable)
|
||||
* @deprecated Use {@linkcode ISessionCommands.getAppiumSessionCapabilities} instead for getting the capabilities.
|
||||
* Use {@linkcode EventCommands.getLogEvents} instead to get the event history.
|
||||
*/
|
||||
async getSession() {
|
||||
return (
|
||||
@@ -366,6 +369,13 @@ export class BaseDriver<
|
||||
) as SingularSessionData<C, SessionData>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns capabilities for the session
|
||||
*/
|
||||
async getAppiumSessionCapabilities(): Promise<SessionCapabilities<C>> {
|
||||
return {capabilities: this.caps};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async deleteSession(sessionId?: string | null) {
|
||||
await this.clearNewCommandTimeout();
|
||||
|
||||
@@ -373,29 +373,33 @@ export function duplicateKeys(input, firstKey, secondKey) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a desired capability and tries to JSON.parse it as an array,
|
||||
* Takes a capability value and tries to JSON.parse it as an array,
|
||||
* and either returns the parsed array or a singleton array.
|
||||
*
|
||||
* @param {string|Array<String>} cap A desired capability
|
||||
* @param {string|string[]} capValue Capability value
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function parseCapsArray(cap) {
|
||||
if (_.isArray(cap)) {
|
||||
return cap;
|
||||
export function parseCapsArray(capValue) {
|
||||
if (_.isArray(capValue)) {
|
||||
return capValue;
|
||||
}
|
||||
|
||||
let parsedCaps;
|
||||
try {
|
||||
parsedCaps = JSON.parse(cap);
|
||||
if (_.isArray(parsedCaps)) {
|
||||
return parsedCaps;
|
||||
const parsed = JSON.parse(capValue);
|
||||
if (_.isArray(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to parse capability as JSON array: ${e.message}`);
|
||||
const message = `Failed to parse capability as JSON array: ${e.message}`;
|
||||
if (_.isString(capValue) && _.startsWith(_.trimStart(capValue), '[')) {
|
||||
throw new TypeError(message);
|
||||
}
|
||||
logger.warn(message);
|
||||
}
|
||||
if (_.isString(cap)) {
|
||||
return [cap];
|
||||
if (_.isString(capValue)) {
|
||||
return [capValue];
|
||||
}
|
||||
throw new Error(`must provide a string or JSON Array; received ${cap}`);
|
||||
throw new TypeError(`Expected a string or a valid JSON array; received '${capValue}'`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -76,7 +76,7 @@ async function createServer (app, cliArgs) {
|
||||
* @param {ServerOpts} opts
|
||||
* @returns {Promise<AppiumServer>}
|
||||
*/
|
||||
async function server(opts) {
|
||||
export async function server(opts) {
|
||||
const {
|
||||
routeConfiguringFunction,
|
||||
port,
|
||||
@@ -87,6 +87,7 @@ async function server(opts) {
|
||||
extraMethodMap = {},
|
||||
serverUpdaters = [],
|
||||
keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS,
|
||||
requestTimeout,
|
||||
} = opts;
|
||||
|
||||
const app = express();
|
||||
@@ -123,7 +124,13 @@ async function server(opts) {
|
||||
// want to block extensions' ability to add routes if they want.
|
||||
app.all('*', catch404Handler);
|
||||
|
||||
await startServer({httpServer, hostname, port, keepAliveTimeout});
|
||||
await startServer({
|
||||
httpServer,
|
||||
hostname,
|
||||
port,
|
||||
keepAliveTimeout,
|
||||
requestTimeout,
|
||||
});
|
||||
|
||||
resolve(appiumServer);
|
||||
} catch (err) {
|
||||
@@ -136,7 +143,7 @@ async function server(opts) {
|
||||
* Sets up some Express middleware and stuff
|
||||
* @param {ConfigureServerOpts} opts
|
||||
*/
|
||||
function configureServer({
|
||||
export function configureServer({
|
||||
app,
|
||||
addRoutes,
|
||||
allowCors = true,
|
||||
@@ -267,13 +274,22 @@ function configureHttp({httpServer, reject, keepAliveTimeout, gracefulShutdownTi
|
||||
* @param {StartServerOpts} opts
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function startServer({httpServer, port, hostname, keepAliveTimeout}) {
|
||||
async function startServer({
|
||||
httpServer,
|
||||
port,
|
||||
hostname,
|
||||
keepAliveTimeout,
|
||||
requestTimeout,
|
||||
}) {
|
||||
// If the hostname is omitted, the server will accept
|
||||
// connections on any IP address
|
||||
/** @type {(port: number, hostname?: string) => B<http.Server>} */
|
||||
const start = B.promisify(httpServer.listen, {context: httpServer});
|
||||
const startPromise = start(port, hostname);
|
||||
httpServer.keepAliveTimeout = keepAliveTimeout;
|
||||
if (_.isInteger(requestTimeout)) {
|
||||
httpServer.requestTimeout = Number(requestTimeout);
|
||||
}
|
||||
// headers timeout must be greater than keepAliveTimeout
|
||||
httpServer.headersTimeout = keepAliveTimeout + 5 * 1000;
|
||||
await startPromise;
|
||||
@@ -284,7 +300,7 @@ async function startServer({httpServer, port, hostname, keepAliveTimeout}) {
|
||||
* @param {string} basePath
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeBasePath(basePath) {
|
||||
export function normalizeBasePath(basePath) {
|
||||
if (!_.isString(basePath)) {
|
||||
throw new Error(`Invalid path prefix ${basePath}`);
|
||||
}
|
||||
@@ -302,7 +318,6 @@ function normalizeBasePath(basePath) {
|
||||
return basePath;
|
||||
}
|
||||
|
||||
export {server, configureServer, normalizeBasePath};
|
||||
|
||||
/**
|
||||
* Options for {@linkcode startServer}.
|
||||
@@ -311,6 +326,8 @@ export {server, configureServer, normalizeBasePath};
|
||||
* @property {number} port - Port to run on
|
||||
* @property {number} keepAliveTimeout - Keep-alive timeout in milliseconds
|
||||
* @property {string} [hostname] - Optional hostname
|
||||
* @property {number} [requestTimeout] - The timeout value in milliseconds for
|
||||
* receiving the entire request from the client
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -344,6 +361,7 @@ export {server, configureServer, normalizeBasePath};
|
||||
* @property {MethodMap} [extraMethodMap]
|
||||
* @property {import('@appium/types').UpdateServerCallback[]} [serverUpdaters]
|
||||
* @property {number} [keepAliveTimeout]
|
||||
* @property {number} [requestTimeout]
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,6 +49,9 @@ export {
|
||||
// Web socket helpers
|
||||
export {DEFAULT_WS_PATHNAME_PREFIX} from './express/websocket';
|
||||
|
||||
// BiDi exports
|
||||
export {BIDI_COMMANDS} from './protocol/bidi-commands';
|
||||
|
||||
export {generateDriverLogPrefix} from './basedriver/helpers';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
```
|
||||
@@ -43,6 +43,11 @@ const {MJSONWP, W3C} = PROTOCOLS;
|
||||
const DEFAULT_LOG = logger.getLogger('Protocol Converter');
|
||||
|
||||
class ProtocolConverter {
|
||||
/**
|
||||
*
|
||||
* @param {ProxyFunction} proxyFunc
|
||||
* @param {import('@appium/types').AppiumLogger | null} [log=null]
|
||||
*/
|
||||
constructor(proxyFunc, log = null) {
|
||||
this.proxyFunc = proxyFunc;
|
||||
this._downstreamProtocol = null;
|
||||
@@ -67,7 +72,7 @@ class ProtocolConverter {
|
||||
* provided in the request, we need to do 3 proxies and combine the result
|
||||
*
|
||||
* @param {Object} body Request body
|
||||
* @return {Array} Array of W3C + MJSONWP compatible timeout objects
|
||||
* @return {Object[]} Array of W3C + MJSONWP compatible timeout objects
|
||||
*/
|
||||
getTimeoutRequestObjects(body) {
|
||||
if (this.downstreamProtocol === W3C && _.has(body, 'ms') && _.has(body, 'type')) {
|
||||
@@ -99,9 +104,10 @@ class ProtocolConverter {
|
||||
|
||||
/**
|
||||
* Proxy an array of timeout objects and merge the result
|
||||
* @param {String} url Endpoint url
|
||||
* @param {String} method Endpoint method
|
||||
* @param {Object} body Request body
|
||||
* @param {string} url Endpoint url
|
||||
* @param {string} method Endpoint method
|
||||
* @param {import('@appium/types').HTTPBody} body Request body
|
||||
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
|
||||
*/
|
||||
async proxySetTimeouts(url, method, body) {
|
||||
let response, resBody;
|
||||
@@ -127,9 +133,16 @@ class ProtocolConverter {
|
||||
|
||||
// ...Otherwise, continue to the next timeouts call
|
||||
}
|
||||
return [response, resBody];
|
||||
return [/** @type {import('@appium/types').ProxyResponse} */(response), resBody];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} method
|
||||
* @param {import('@appium/types').HTTPBody} body
|
||||
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
|
||||
*/
|
||||
async proxySetWindow(url, method, body) {
|
||||
const bodyObj = util.safeJsonParse(body);
|
||||
if (_.isPlainObject(bodyObj)) {
|
||||
@@ -160,6 +173,13 @@ class ProtocolConverter {
|
||||
return await this.proxyFunc(url, method, body);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} method
|
||||
* @param {import('@appium/types').HTTPBody} body
|
||||
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
|
||||
*/
|
||||
async proxySetValue(url, method, body) {
|
||||
const bodyObj = util.safeJsonParse(body);
|
||||
if (_.isPlainObject(bodyObj) && (util.hasValue(bodyObj.text) || util.hasValue(bodyObj.value))) {
|
||||
@@ -186,6 +206,13 @@ class ProtocolConverter {
|
||||
return await this.proxyFunc(url, method, body);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} method
|
||||
* @param {import('@appium/types').HTTPBody} body
|
||||
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
|
||||
*/
|
||||
async proxySetFrame(url, method, body) {
|
||||
const bodyObj = util.safeJsonParse(body);
|
||||
return _.has(bodyObj, 'id') && _.isPlainObject(bodyObj.id)
|
||||
@@ -196,6 +223,13 @@ class ProtocolConverter {
|
||||
: await this.proxyFunc(url, method, body);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} method
|
||||
* @param {import('@appium/types').HTTPBody} body
|
||||
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
|
||||
*/
|
||||
async proxyPerformActions(url, method, body) {
|
||||
const bodyObj = util.safeJsonParse(body);
|
||||
return _.isPlainObject(bodyObj)
|
||||
@@ -207,6 +241,12 @@ class ProtocolConverter {
|
||||
: await this.proxyFunc(url, method, body);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} method
|
||||
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
|
||||
*/
|
||||
async proxyReleaseActions(url, method) {
|
||||
return await this.proxyFunc(url, method);
|
||||
}
|
||||
@@ -218,8 +258,8 @@ class ProtocolConverter {
|
||||
* @param {string} commandName
|
||||
* @param {string} url
|
||||
* @param {string} method
|
||||
* @param {?string|object} body
|
||||
* @returns The proxyfying result as [response, responseBody] tuple
|
||||
* @param {import('@appium/types').HTTPBody} [body]
|
||||
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
|
||||
*/
|
||||
async convertAndProxy(commandName, url, method, body) {
|
||||
if (!this.downstreamProtocol) {
|
||||
@@ -272,3 +312,7 @@ class ProtocolConverter {
|
||||
}
|
||||
|
||||
export default ProtocolConverter;
|
||||
|
||||
/**
|
||||
* @typedef {(url: string, method: string, body?: import('@appium/types').HTTPBody) => Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>} ProxyFunction
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import axios from 'axios';
|
||||
import { CancellationError } from 'bluebird';
|
||||
import EventEmitter from 'node:events';
|
||||
|
||||
const CANCEL_EVENT = 'cancel';
|
||||
const FINISH_EVENT = 'finish';
|
||||
|
||||
export class ProxyRequest {
|
||||
private readonly _requestConfig: axios.RawAxiosRequestConfig;
|
||||
private readonly _ee: EventEmitter;
|
||||
private _resultPromise: Promise<any> | null;
|
||||
|
||||
constructor(requestConfig: axios.RawAxiosRequestConfig<any>) {
|
||||
this._requestConfig = requestConfig;
|
||||
this._ee = new EventEmitter();
|
||||
this._resultPromise = null;
|
||||
}
|
||||
|
||||
async execute(): Promise<axios.AxiosResponse> {
|
||||
if (this._resultPromise) {
|
||||
return await this._resultPromise;
|
||||
}
|
||||
|
||||
try {
|
||||
this._resultPromise = Promise.race([
|
||||
this._makeRacingTimer(),
|
||||
this._makeRequest(),
|
||||
]);
|
||||
return await this._resultPromise;
|
||||
} finally {
|
||||
this._ee.emit(FINISH_EVENT);
|
||||
this._ee.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this._ee.emit(CANCEL_EVENT);
|
||||
}
|
||||
|
||||
private async _makeRequest(): Promise<axios.AxiosResponse> {
|
||||
return await axios(this._requestConfig);
|
||||
}
|
||||
|
||||
private async _makeRacingTimer(): Promise<void> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
this._ee.once(FINISH_EVENT, resolve);
|
||||
this._ee.once(CANCEL_EVENT, () => reject(new CancellationError(
|
||||
'The request has been cancelled'
|
||||
)));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import {logger, util} from '@appium/support';
|
||||
import axios from 'axios';
|
||||
import {getSummaryByCode} from '../jsonwp-status/status';
|
||||
import {
|
||||
errors,
|
||||
@@ -9,15 +8,19 @@ import {
|
||||
errorFromW3CJsonCode,
|
||||
getResponseForW3CError,
|
||||
} from '../protocol/errors';
|
||||
import {routeToCommandName} from '../protocol';
|
||||
import {isSessionCommand, routeToCommandName} from '../protocol';
|
||||
import {MAX_LOG_BODY_LENGTH, DEFAULT_BASE_PATH, PROTOCOLS} from '../constants';
|
||||
import ProtocolConverter from './protocol-converter';
|
||||
import {formatResponseValue, formatStatus} from '../protocol/helpers';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import { match as pathToRegexMatch } from 'path-to-regexp';
|
||||
import nodeUrl from 'node:url';
|
||||
import { ProxyRequest } from './proxy-request';
|
||||
|
||||
const DEFAULT_LOG = logger.getLogger('WD Proxy');
|
||||
const DEFAULT_REQUEST_TIMEOUT = 240000;
|
||||
const COMMAND_WITH_SESSION_ID_MATCHER = pathToRegexMatch('{/*prefix}/session/:sessionId{/*command}');
|
||||
|
||||
const {MJSONWP, W3C} = PROTOCOLS;
|
||||
|
||||
@@ -33,7 +36,7 @@ const ALLOWED_OPTS = [
|
||||
'keepAlive',
|
||||
];
|
||||
|
||||
class JWProxy {
|
||||
export class JWProxy {
|
||||
/** @type {string} */
|
||||
scheme;
|
||||
/** @type {string} */
|
||||
@@ -48,13 +51,20 @@ class JWProxy {
|
||||
sessionId;
|
||||
/** @type {number} */
|
||||
timeout;
|
||||
/** @type {Protocol | null | undefined} */
|
||||
_downstreamProtocol;
|
||||
/** @type {ProxyRequest[]} */
|
||||
_activeRequests;
|
||||
|
||||
/**
|
||||
* @param {import('@appium/types').ProxyOptions} [opts={}]
|
||||
*/
|
||||
constructor(opts = {}) {
|
||||
opts = _.pick(opts, ALLOWED_OPTS);
|
||||
|
||||
const filteredOpts = _.pick(opts, ALLOWED_OPTS);
|
||||
// omit 'log' in the defaults assignment here because 'log' is a getter and we are going to set
|
||||
// it to this._log (which lies behind the getter) further down
|
||||
const options = _.defaults(_.omit(opts, 'log'), {
|
||||
/** @type {import('@appium/types').ProxyOptions} */
|
||||
const options = _.defaults(_.omit(filteredOpts, 'log'), {
|
||||
scheme: 'http',
|
||||
server: 'localhost',
|
||||
port: 4444,
|
||||
@@ -63,7 +73,7 @@ class JWProxy {
|
||||
sessionId: null,
|
||||
timeout: DEFAULT_REQUEST_TIMEOUT,
|
||||
});
|
||||
options.scheme = options.scheme.toLowerCase();
|
||||
options.scheme = /** @type {string} */ (options.scheme).toLowerCase();
|
||||
Object.assign(this, options);
|
||||
|
||||
this._activeRequests = [];
|
||||
@@ -77,6 +87,8 @@ class JWProxy {
|
||||
this.httpsAgent = new https.Agent(agentOpts);
|
||||
this.protocolConverter = new ProtocolConverter(this.proxy.bind(this), opts.log);
|
||||
this._log = opts.log;
|
||||
|
||||
this.log.debug(`${this.constructor.name} options: ${JSON.stringify(options)}`);
|
||||
}
|
||||
|
||||
get log() {
|
||||
@@ -93,27 +105,32 @@ class JWProxy {
|
||||
* @returns {Promise<import('axios').AxiosResponse>}
|
||||
*/
|
||||
async request(requestConfig) {
|
||||
const reqPromise = axios(requestConfig);
|
||||
this._activeRequests.push(reqPromise);
|
||||
const req = new ProxyRequest(requestConfig);
|
||||
this._activeRequests.push(req);
|
||||
try {
|
||||
return await reqPromise;
|
||||
return await req.execute();
|
||||
} finally {
|
||||
_.pull(this._activeRequests, reqPromise);
|
||||
_.pull(this._activeRequests, req);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
getActiveRequestsCount() {
|
||||
return this._activeRequests.length;
|
||||
}
|
||||
|
||||
cancelActiveRequests() {
|
||||
for (const ar of this._activeRequests) {
|
||||
ar.cancel();
|
||||
}
|
||||
this._activeRequests = [];
|
||||
}
|
||||
|
||||
endpointRequiresSessionId(endpoint) {
|
||||
return !_.includes(['/session', '/sessions', '/status'], endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Protocol | null | undefined} value
|
||||
*/
|
||||
set downstreamProtocol(value) {
|
||||
this._downstreamProtocol = value;
|
||||
this.protocolConverter.downstreamProtocol = value;
|
||||
@@ -123,67 +140,46 @@ class JWProxy {
|
||||
return this._downstreamProtocol;
|
||||
}
|
||||
|
||||
getUrlForProxy(url) {
|
||||
if (url === '') {
|
||||
url = '/';
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} [method]
|
||||
* @returns {string}
|
||||
*/
|
||||
getUrlForProxy(url, method) {
|
||||
const parsedUrl = this._parseUrl(url);
|
||||
const normalizedPathname = this._toNormalizedPathname(parsedUrl);
|
||||
const commandName = normalizedPathname
|
||||
? routeToCommandName(
|
||||
normalizedPathname,
|
||||
/** @type {import('@appium/types').HTTPMethod | undefined} */ (method)
|
||||
)
|
||||
: '';
|
||||
const requiresSessionId = !commandName || (commandName && isSessionCommand(commandName));
|
||||
const proxyPrefix = `${this.scheme}://${this.server}:${this.port}${this.base}`;
|
||||
let proxySuffix = normalizedPathname ? `/${_.trimStart(normalizedPathname, '/')}` : '';
|
||||
if (parsedUrl.search) {
|
||||
proxySuffix += parsedUrl.search;
|
||||
}
|
||||
const proxyBase = `${this.scheme}://${this.server}:${this.port}${this.base}`;
|
||||
const endpointRe = '(/(session|status))';
|
||||
let remainingUrl = '';
|
||||
if (/^http/.test(url)) {
|
||||
const first = new RegExp(`(https?://.+)${endpointRe}`).exec(url);
|
||||
if (!first) {
|
||||
throw new Error('Got a complete url but could not extract JWP endpoint');
|
||||
}
|
||||
remainingUrl = url.replace(first[1], '');
|
||||
} else if (new RegExp('^/').test(url)) {
|
||||
remainingUrl = url;
|
||||
} else {
|
||||
throw new Error(`Did not know what to do with url '${url}'`);
|
||||
if (!requiresSessionId) {
|
||||
return `${proxyPrefix}${proxySuffix}`;
|
||||
}
|
||||
|
||||
const stripPrefixRe = new RegExp('^.*?(/(session|status).*)$');
|
||||
if (stripPrefixRe.test(remainingUrl)) {
|
||||
remainingUrl = /** @type {RegExpExecArray} */ (stripPrefixRe.exec(remainingUrl))[1];
|
||||
if (!this.sessionId) {
|
||||
throw new ReferenceError(`Session ID is not set, but saw a URL that requires it (${url})`);
|
||||
}
|
||||
|
||||
if (!new RegExp(endpointRe).test(remainingUrl)) {
|
||||
remainingUrl = `/session/${this.sessionId}${remainingUrl}`;
|
||||
}
|
||||
|
||||
const requiresSessionId = this.endpointRequiresSessionId(remainingUrl);
|
||||
|
||||
if (requiresSessionId && this.sessionId === null) {
|
||||
throw new Error('Trying to proxy a session command without session id');
|
||||
}
|
||||
|
||||
const sessionBaseRe = new RegExp('^/session/([^/]+)');
|
||||
if (sessionBaseRe.test(remainingUrl)) {
|
||||
if (this.sessionId === null) {
|
||||
throw new ReferenceError(
|
||||
`Session ID is not set, but saw a URL path referencing a session (${remainingUrl}). This may be a bug in your client.`
|
||||
);
|
||||
}
|
||||
// we have something like /session/:id/foobar, so we need to replace
|
||||
// the session id
|
||||
const match = sessionBaseRe.exec(remainingUrl);
|
||||
// TODO: if `requiresSessionId` is `false` and `sessionId` is `null`, this is a bug.
|
||||
// are we sure `sessionId` is not `null`?
|
||||
remainingUrl = remainingUrl.replace(
|
||||
/** @type {RegExpExecArray} */ (match)[1],
|
||||
/** @type {string} */ (this.sessionId)
|
||||
);
|
||||
} else if (requiresSessionId) {
|
||||
throw new Error(`Could not find :session section for url: ${remainingUrl}`);
|
||||
}
|
||||
remainingUrl = remainingUrl.replace(/\/$/, ''); // can't have trailing slashes
|
||||
|
||||
return proxyBase + remainingUrl;
|
||||
return `${proxyPrefix}/session/${this.sessionId}${proxySuffix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} method
|
||||
* @param {import('@appium/types').HTTPBody} [body=null]
|
||||
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
|
||||
*/
|
||||
async proxy(url, method, body = null) {
|
||||
method = method.toUpperCase();
|
||||
const newUrl = this.getUrlForProxy(url);
|
||||
const newUrl = this.getUrlForProxy(url, method);
|
||||
const truncateBody = (content) =>
|
||||
_.truncate(_.isString(content) ? content : JSON.stringify(content), {
|
||||
length: MAX_LOG_BODY_LENGTH,
|
||||
@@ -252,8 +248,12 @@ class JWProxy {
|
||||
// Some servers, like chromedriver may return response code 200 for non-zero JSONWP statuses
|
||||
throwProxyError(data);
|
||||
}
|
||||
const res = {statusCode: status, headers, body: data};
|
||||
return [res, data];
|
||||
const headersMap = /** @type {import('@appium/types').HTTPHeaders} */ (headers);
|
||||
return [{
|
||||
statusCode: status,
|
||||
headers: headersMap,
|
||||
body: data,
|
||||
}, data];
|
||||
} catch (e) {
|
||||
// We only consider an error unexpected if this was not
|
||||
// an async request module error or if the response cannot be cast to
|
||||
@@ -276,6 +276,11 @@ class JWProxy {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Record<string, any>} resObj
|
||||
* @returns {Protocol | undefined}
|
||||
*/
|
||||
getProtocolFromResBody(resObj) {
|
||||
if (_.isInteger(resObj.status)) {
|
||||
return MJSONWP;
|
||||
@@ -286,6 +291,7 @@ class JWProxy {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This method is not used anymore and will be removed
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {import('@appium/types').HTTPMethod} method
|
||||
@@ -319,10 +325,13 @@ class JWProxy {
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {import('@appium/types').HTTPMethod} method
|
||||
* @param {any?} body
|
||||
* @param {import('@appium/types').HTTPBody} [body=null]
|
||||
* @returns {Promise<[import('@appium/types').ProxyResponse, import('@appium/types').HTTPBody]>}
|
||||
*/
|
||||
async proxyCommand(url, method, body = null) {
|
||||
const commandName = this.requestToCommandName(url, method);
|
||||
const parsedUrl = this._parseUrl(url);
|
||||
const normalizedPathname = this._toNormalizedPathname(parsedUrl);
|
||||
const commandName = normalizedPathname ? routeToCommandName(normalizedPathname, method) : '';
|
||||
if (!commandName) {
|
||||
return await this.proxy(url, method, body);
|
||||
}
|
||||
@@ -335,8 +344,8 @@ class JWProxy {
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {import('@appium/types').HTTPMethod} method
|
||||
* @param {any?} body
|
||||
* @returns {Promise<unknown>}
|
||||
* @param {import('@appium/types').HTTPBody} [body=null]
|
||||
* @returns {Promise<import('@appium/types').HTTPBody>}
|
||||
*/
|
||||
async command(url, method, body = null) {
|
||||
let response;
|
||||
@@ -390,20 +399,40 @@ class JWProxy {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @returns {string | null}
|
||||
*/
|
||||
getSessionIdFromUrl(url) {
|
||||
const match = url.match(/\/session\/([^/]+)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
*/
|
||||
async proxyReqRes(req, res) {
|
||||
// ! this method must not throw any exceptions
|
||||
// ! make sure to call res.send before return
|
||||
/** @type {number} */
|
||||
let statusCode;
|
||||
/** @type {import('@appium/types').HTTPBody} */
|
||||
let resBodyObj;
|
||||
try {
|
||||
let response;
|
||||
[response, resBodyObj] = await this.proxyCommand(req.originalUrl, req.method, req.body);
|
||||
res.headers = response.headers;
|
||||
[response, resBodyObj] = await this.proxyCommand(
|
||||
req.originalUrl,
|
||||
/** @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(
|
||||
@@ -435,11 +464,57 @@ class JWProxy {
|
||||
resBodyObj.value = formatResponseValue(resBodyObj.value);
|
||||
res.status(statusCode).send(JSON.stringify(formatStatus(resBodyObj)));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @returns {ParsedUrl}
|
||||
*/
|
||||
_parseUrl(url) {
|
||||
const parsedUrl = nodeUrl.parse(url || '/');
|
||||
if (
|
||||
_.isNil(parsedUrl.href) || _.isNil(parsedUrl.pathname)
|
||||
|| (parsedUrl.protocol && !['http:', 'https:'].includes(parsedUrl.protocol))
|
||||
) {
|
||||
throw new Error(`Did not know how to proxy the url '${url}'`);
|
||||
}
|
||||
return parsedUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ParsedUrl} parsedUrl
|
||||
* @returns {string}
|
||||
*/
|
||||
_toNormalizedPathname(parsedUrl) {
|
||||
if (!_.isString(parsedUrl.pathname)) {
|
||||
return '';
|
||||
}
|
||||
let pathname = this.reqBasePath && parsedUrl.pathname.startsWith(this.reqBasePath)
|
||||
? parsedUrl.pathname.replace(this.reqBasePath, '')
|
||||
: parsedUrl.pathname;
|
||||
const match = COMMAND_WITH_SESSION_ID_MATCHER(pathname);
|
||||
// This is needed for the backward compatibility
|
||||
// if drivers don't set reqBasePath properly
|
||||
if (!this.reqBasePath) {
|
||||
if (match && _.isArray(match.params?.prefix)) {
|
||||
pathname = pathname.replace(`/${match.params?.prefix.join('/')}`, '');
|
||||
} else if (_.startsWith(pathname, '/wd/hub')) {
|
||||
pathname = pathname.replace('/wd/hub', '');
|
||||
}
|
||||
}
|
||||
let result = pathname;
|
||||
if (match) {
|
||||
result = _.isArray(match.params?.command) ? `/${match.params.command.join('/')}` : '';
|
||||
}
|
||||
return _.trimEnd(result, '/');
|
||||
}
|
||||
}
|
||||
|
||||
export {JWProxy};
|
||||
export default JWProxy;
|
||||
|
||||
/**
|
||||
* @typedef {Error & {response: {data: import('type-fest').JsonObject, status: import('http-status-codes').StatusCodes}}} ProxyError
|
||||
* @typedef {nodeUrl.UrlWithStringQuery} ParsedUrl
|
||||
* @typedef {typeof PROTOCOLS[keyof typeof PROTOCOLS]} Protocol
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -3,7 +3,7 @@ const SUBSCRIPTION_REQUEST_PARAMS = /** @type {const} */ ({
|
||||
optional: ['contexts'],
|
||||
});
|
||||
|
||||
const BIDI_COMMANDS = /** @type {const} */ ({
|
||||
export const BIDI_COMMANDS = /** @type {const} */ ({
|
||||
session: {
|
||||
subscribe: {
|
||||
command: 'bidiSubscribe',
|
||||
@@ -31,5 +31,3 @@ const BIDI_COMMANDS = /** @type {const} */ ({
|
||||
|
||||
// TODO add definitions for all bidi commands.
|
||||
// spec link: https://w3c.github.io/webdriver-bidi/
|
||||
|
||||
export {BIDI_COMMANDS};
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
CREATE_SESSION_COMMAND,
|
||||
DELETE_SESSION_COMMAND,
|
||||
GET_STATUS_COMMAND,
|
||||
LIST_DRIVER_COMMANDS_COMMAND,
|
||||
LIST_DRIVER_EXTENSIONS_COMMAND,
|
||||
makeArgs,
|
||||
checkParams,
|
||||
validateExecuteMethodParams,
|
||||
@@ -30,4 +32,6 @@ export {
|
||||
CREATE_SESSION_COMMAND,
|
||||
DELETE_SESSION_COMMAND,
|
||||
GET_STATUS_COMMAND,
|
||||
LIST_DRIVER_COMMANDS_COMMAND,
|
||||
LIST_DRIVER_EXTENSIONS_COMMAND,
|
||||
};
|
||||
|
||||
@@ -16,9 +16,11 @@ import {isW3cCaps} from '../helpers/capabilities';
|
||||
import log from '../basedriver/logger';
|
||||
import { generateDriverLogPrefix } from '../basedriver/helpers';
|
||||
|
||||
const CREATE_SESSION_COMMAND = 'createSession';
|
||||
const DELETE_SESSION_COMMAND = 'deleteSession';
|
||||
const GET_STATUS_COMMAND = 'getStatus';
|
||||
export const CREATE_SESSION_COMMAND = 'createSession';
|
||||
export const DELETE_SESSION_COMMAND = 'deleteSession';
|
||||
export const GET_STATUS_COMMAND = 'getStatus';
|
||||
export const LIST_DRIVER_COMMANDS_COMMAND = 'listCommands';
|
||||
export const LIST_DRIVER_EXTENSIONS_COMMAND = 'listExtensions';
|
||||
|
||||
/** @type {Set<string>} */
|
||||
const deprecatedCommandsLogged = new Set();
|
||||
@@ -559,9 +561,6 @@ export {
|
||||
isSessionCommand,
|
||||
driverShouldDoJwpProxy,
|
||||
determineProtocol,
|
||||
CREATE_SESSION_COMMAND,
|
||||
DELETE_SESSION_COMMAND,
|
||||
GET_STATUS_COMMAND,
|
||||
deprecatedCommandsLogged,
|
||||
validateExecuteMethodParams,
|
||||
};
|
||||
|
||||
@@ -4,6 +4,12 @@ import _ from 'lodash';
|
||||
import {util} from '@appium/support';
|
||||
import {PROTOCOLS, DEFAULT_BASE_PATH} from '../constants';
|
||||
import {match} from 'path-to-regexp';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
|
||||
/** @type {LRUCache<string, string>} */
|
||||
const COMMAND_NAMES_CACHE = new LRUCache({
|
||||
max: 1024,
|
||||
});
|
||||
|
||||
const SET_ALERT_TEXT_PAYLOAD_PARAMS = {
|
||||
validate: (jsonObj) =>
|
||||
@@ -22,6 +28,12 @@ const SET_ALERT_TEXT_PAYLOAD_PARAMS = {
|
||||
* @satisfies {import('@appium/types').MethodMap<import('../basedriver/driver').BaseDriver>}
|
||||
*/
|
||||
export const METHOD_MAP = /** @type {const} */ ({
|
||||
|
||||
//
|
||||
// W3C WebDriver (and deprecated MJSONWP that will be removed in Appium 3)
|
||||
// https://www.w3.org/TR/webdriver1/
|
||||
// https://www.w3.org/TR/webdriver2/
|
||||
//
|
||||
'/status': {
|
||||
GET: {command: 'getStatus'},
|
||||
},
|
||||
@@ -29,6 +41,7 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
POST: {
|
||||
command: 'createSession',
|
||||
payloadParams: {
|
||||
// TODO: Appium 3 will accept only 'capabilities'.
|
||||
validate: (jsonObj) =>
|
||||
!jsonObj.capabilities &&
|
||||
!jsonObj.desiredCapabilities &&
|
||||
@@ -37,11 +50,8 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
},
|
||||
},
|
||||
},
|
||||
'/sessions': {
|
||||
GET: {command: 'getSessions'},
|
||||
},
|
||||
'/session/:sessionId': {
|
||||
GET: {command: 'getSession'},
|
||||
GET: {command: 'getSession', deprecated: true},
|
||||
DELETE: {command: 'deleteSession'},
|
||||
},
|
||||
'/session/:sessionId/timeouts': {
|
||||
@@ -59,7 +69,7 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
return 'W3C protocol expects any of script, pageLoad or implicit to be set';
|
||||
}
|
||||
} else {
|
||||
// MJSONWP
|
||||
// TODO: Remove in Appium 3
|
||||
if (!util.hasValue(jsonObj.type) || !util.hasValue(jsonObj.ms)) {
|
||||
return 'MJSONWP protocol requires type and ms';
|
||||
}
|
||||
@@ -69,25 +79,6 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/timeouts/async_script': {
|
||||
POST: {command: 'asyncScriptTimeout', payloadParams: {required: ['ms']}, deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/timeouts/implicit_wait': {
|
||||
POST: {command: 'implicitWait', payloadParams: {required: ['ms']}, deprecated: true},
|
||||
},
|
||||
// JSONWP
|
||||
'/session/:sessionId/window_handle': {
|
||||
GET: {command: 'getWindowHandle'},
|
||||
},
|
||||
// W3C
|
||||
'/session/:sessionId/window/handle': {
|
||||
GET: {command: 'getWindowHandle'},
|
||||
},
|
||||
// JSONWP
|
||||
'/session/:sessionId/window_handles': {
|
||||
GET: {command: 'getWindowHandles'},
|
||||
},
|
||||
// W3C
|
||||
'/session/:sessionId/window/handles': {
|
||||
GET: {command: 'getWindowHandles'},
|
||||
},
|
||||
@@ -104,39 +95,10 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
'/session/:sessionId/refresh': {
|
||||
POST: {command: 'refresh'},
|
||||
},
|
||||
// MJSONWP
|
||||
'/session/:sessionId/execute': {
|
||||
POST: {command: 'execute', payloadParams: {required: ['script', 'args']}},
|
||||
},
|
||||
// MJSONWP
|
||||
'/session/:sessionId/execute_async': {
|
||||
POST: {
|
||||
command: 'executeAsync',
|
||||
payloadParams: {required: ['script', 'args']},
|
||||
},
|
||||
},
|
||||
|
||||
'/session/:sessionId/screenshot': {
|
||||
GET: {command: 'getScreenshot'},
|
||||
},
|
||||
'/session/:sessionId/ime/available_engines': {
|
||||
GET: {command: 'availableIMEEngines', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/ime/active_engine': {
|
||||
GET: {command: 'getActiveIMEEngine', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/ime/activated': {
|
||||
GET: {command: 'isIMEActivated', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/ime/deactivate': {
|
||||
POST: {command: 'deactivateIMEEngine', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/ime/activate': {
|
||||
POST: {
|
||||
command: 'activateIMEEngine',
|
||||
payloadParams: {required: ['engine']},
|
||||
deprecated: true,
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/frame': {
|
||||
POST: {command: 'setFrame', payloadParams: {required: ['id']}},
|
||||
},
|
||||
@@ -148,6 +110,7 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
POST: {
|
||||
command: 'setWindow',
|
||||
payloadParams: {
|
||||
// TODO: Appium 3 will only accept 'handle'. 'name' will be ginored.
|
||||
optional: ['name', 'handle'],
|
||||
// Return both values to match W3C and JSONWP protocols
|
||||
makeArgs: (jsonObj) => {
|
||||
@@ -167,16 +130,18 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
},
|
||||
DELETE: {command: 'closeWindow'},
|
||||
},
|
||||
'/session/:sessionId/window/:windowhandle/size': {
|
||||
GET: {command: 'getWindowSize', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/window/:windowhandle/position': {
|
||||
POST: {deprecated: true},
|
||||
GET: {deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/window/:windowhandle/maximize': {
|
||||
'/session/:sessionId/window/maximize': {
|
||||
POST: {command: 'maximizeWindow'},
|
||||
},
|
||||
'/session/:sessionId/window/minimize': {
|
||||
POST: {command: 'minimizeWindow'},
|
||||
},
|
||||
'/session/:sessionId/window/fullscreen': {
|
||||
POST: {command: 'fullScreenWindow'},
|
||||
},
|
||||
'/session/:sessionId/window/new': {
|
||||
POST: {command: 'createNewWindow', payloadParams: {optional: ['type']}},
|
||||
},
|
||||
'/session/:sessionId/cookie': {
|
||||
GET: {command: 'getCookies'},
|
||||
POST: {command: 'setCookie', payloadParams: {required: ['cookie']}},
|
||||
@@ -206,7 +171,7 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
},
|
||||
'/session/:sessionId/element/active': {
|
||||
GET: {command: 'active'}, // W3C: https://w3c.github.io/webdriver/webdriver-spec.html#dfn-get-active-element
|
||||
POST: {command: 'active'},
|
||||
POST: {command: 'active', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId': {
|
||||
GET: {},
|
||||
@@ -226,9 +191,6 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
'/session/:sessionId/element/:elementId/click': {
|
||||
POST: {command: 'click'},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/submit': {
|
||||
POST: {command: 'submit', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/text': {
|
||||
GET: {command: 'getText'},
|
||||
},
|
||||
@@ -240,6 +202,7 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
!util.hasValue(jsonObj.value) &&
|
||||
!util.hasValue(jsonObj.text) &&
|
||||
'we require one of "text" or "value" params',
|
||||
// TODO: Appium 3 will accept only 'value'.
|
||||
optional: ['value', 'text'],
|
||||
// override the default argument constructor because of the special
|
||||
// logic here. Basically we want to accept either a value (old JSONWP)
|
||||
@@ -250,9 +213,6 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/keys': {
|
||||
POST: {command: 'keys', payloadParams: {required: ['value']}, deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/name': {
|
||||
GET: {command: 'getName'},
|
||||
},
|
||||
@@ -268,21 +228,9 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
'/session/:sessionId/element/:elementId/attribute/:name': {
|
||||
GET: {command: 'getAttribute'},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/equals/:otherId': {
|
||||
GET: {command: 'equalsElement', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/displayed': {
|
||||
GET: {command: 'elementDisplayed'},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/location': {
|
||||
GET: {command: 'getLocation', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/location_in_view': {
|
||||
GET: {command: 'getLocationInView', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/size': {
|
||||
GET: {command: 'getSize', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/shadow': {
|
||||
GET: {command: 'elementShadowRoot'},
|
||||
},
|
||||
@@ -301,12 +249,8 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
'/session/:sessionId/element/:elementId/css/:propertyName': {
|
||||
GET: {command: 'getCssProperty'},
|
||||
},
|
||||
'/session/:sessionId/orientation': {
|
||||
GET: {command: 'getOrientation'},
|
||||
POST: {
|
||||
command: 'setOrientation',
|
||||
payloadParams: {required: ['orientation']},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/property/:name': {
|
||||
GET: {command: 'getProperty'},
|
||||
},
|
||||
// w3c v2 https://www.w3.org/TR/webdriver2/#get-computed-role
|
||||
'session/:sessionId/element/:elementId/computedrole': {
|
||||
@@ -316,10 +260,336 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
'session/:sessionId/element/:elementId/computedlabel': {
|
||||
GET: {command: 'getComputedLabel'},
|
||||
},
|
||||
'/session/:sessionId/actions': {
|
||||
POST: {command: 'performActions', payloadParams: {required: ['actions']}},
|
||||
DELETE: {command: 'releaseActions'},
|
||||
},
|
||||
'/session/:sessionId/alert/text': {
|
||||
GET: {command: 'getAlertText'},
|
||||
POST: {
|
||||
command: 'setAlertText',
|
||||
payloadParams: SET_ALERT_TEXT_PAYLOAD_PARAMS,
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/alert/accept': {
|
||||
POST: {command: 'postAcceptAlert'},
|
||||
},
|
||||
'/session/:sessionId/alert/dismiss': {
|
||||
POST: {command: 'postDismissAlert'},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/rect': {
|
||||
GET: {command: 'getElementRect'},
|
||||
},
|
||||
'/session/:sessionId/execute/sync': {
|
||||
POST: {command: 'execute', payloadParams: {required: ['script', 'args']}},
|
||||
},
|
||||
'/session/:sessionId/execute/async': {
|
||||
POST: {
|
||||
command: 'executeAsync',
|
||||
payloadParams: {required: ['script', 'args']},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/screenshot': {
|
||||
GET: {command: 'getElementScreenshot'},
|
||||
},
|
||||
'/session/:sessionId/window/rect': {
|
||||
GET: {command: 'getWindowRect'},
|
||||
POST: {
|
||||
command: 'setWindowRect',
|
||||
payloadParams: {optional: ['x', 'y', 'width', 'height']},
|
||||
},
|
||||
},
|
||||
|
||||
//
|
||||
// Appium specific
|
||||
//
|
||||
'/session/:sessionId/ime/available_engines': {
|
||||
GET: {command: 'availableIMEEngines'},
|
||||
},
|
||||
'/session/:sessionId/ime/active_engine': {
|
||||
GET: {command: 'getActiveIMEEngine'},
|
||||
},
|
||||
'/session/:sessionId/ime/activated': {
|
||||
GET: {command: 'isIMEActivated'},
|
||||
},
|
||||
'/session/:sessionId/ime/deactivate': {
|
||||
POST: {command: 'deactivateIMEEngine'},
|
||||
},
|
||||
'/session/:sessionId/ime/activate': {
|
||||
POST: {command: 'activateIMEEngine', payloadParams: {required: ['engine']}},
|
||||
},
|
||||
'/session/:sessionId/rotation': {
|
||||
GET: {command: 'getRotation'},
|
||||
POST: {command: 'setRotation', payloadParams: {required: ['x', 'y', 'z']}},
|
||||
},
|
||||
'/session/:sessionId/location': {
|
||||
GET: {command: 'getGeoLocation'},
|
||||
POST: {command: 'setGeoLocation', payloadParams: {required: ['location']}},
|
||||
},
|
||||
'/session/:sessionId/orientation': {
|
||||
GET: {command: 'getOrientation'},
|
||||
POST: {
|
||||
command: 'setOrientation',
|
||||
payloadParams: {required: ['orientation']}
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/context': {
|
||||
GET: {command: 'getCurrentContext'},
|
||||
POST: {command: 'setContext', payloadParams: {required: ['name']}},
|
||||
},
|
||||
'/session/:sessionId/contexts': {
|
||||
GET: {command: 'getContexts'},
|
||||
},
|
||||
'/session/:sessionId/network_connection': {
|
||||
GET: {command: 'getNetworkConnection'},
|
||||
POST: {
|
||||
command: 'setNetworkConnection',
|
||||
payloadParams: {unwrap: 'parameters', required: ['type']},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/receive_async_response': {
|
||||
POST: {
|
||||
command: 'receiveAsyncResponse',
|
||||
payloadParams: {required: ['status', 'value']},
|
||||
},
|
||||
},
|
||||
'/appium/sessions': {
|
||||
GET: {command: 'getAppiumSessions'},
|
||||
},
|
||||
'/session/:sessionId/appium/capabilities': {
|
||||
GET: {command: 'getAppiumSessionCapabilities'}
|
||||
},
|
||||
'/session/:sessionId/appium/device/system_time': {
|
||||
GET: {command: 'getDeviceTime', payloadParams: {optional: ['format']}},
|
||||
POST: {command: 'getDeviceTime', payloadParams: {optional: ['format']}},
|
||||
},
|
||||
// #region Applications Management
|
||||
'/session/:sessionId/appium/device/install_app': {
|
||||
POST: {
|
||||
command: 'installApp',
|
||||
payloadParams: {
|
||||
required: ['appPath'],
|
||||
optional: ['options'],
|
||||
},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/appium/device/activate_app': {
|
||||
POST: {
|
||||
command: 'activateApp',
|
||||
payloadParams: {
|
||||
required: [['appId'], ['bundleId']],
|
||||
optional: ['options'],
|
||||
},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/appium/device/remove_app': {
|
||||
POST: {
|
||||
command: 'removeApp',
|
||||
payloadParams: {
|
||||
required: [['appId'], ['bundleId']],
|
||||
optional: ['options'],
|
||||
},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/appium/device/terminate_app': {
|
||||
POST: {
|
||||
command: 'terminateApp',
|
||||
payloadParams: {
|
||||
required: [['appId'], ['bundleId']],
|
||||
optional: ['options'],
|
||||
},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/appium/device/app_installed': {
|
||||
POST: {
|
||||
command: 'isAppInstalled',
|
||||
payloadParams: {
|
||||
required: [['appId'], ['bundleId']],
|
||||
},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/appium/device/app_state': {
|
||||
GET: {
|
||||
command: 'queryAppState',
|
||||
payloadParams: {
|
||||
required: [['appId'], ['bundleId']],
|
||||
},
|
||||
},
|
||||
POST: {
|
||||
command: 'queryAppState',
|
||||
payloadParams: {
|
||||
required: [['appId'], ['bundleId']],
|
||||
},
|
||||
deprecated: true,
|
||||
},
|
||||
},
|
||||
// #endregion
|
||||
'/session/:sessionId/appium/device/hide_keyboard': {
|
||||
POST: {
|
||||
command: 'hideKeyboard',
|
||||
payloadParams: {optional: ['strategy', 'key', 'keyCode', 'keyName']},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/appium/device/is_keyboard_shown': {
|
||||
GET: {command: 'isKeyboardShown'},
|
||||
},
|
||||
'/session/:sessionId/appium/device/push_file': {
|
||||
POST: {command: 'pushFile', payloadParams: {required: ['path', 'data']}},
|
||||
},
|
||||
'/session/:sessionId/appium/device/pull_file': {
|
||||
POST: {command: 'pullFile', payloadParams: {required: ['path']}},
|
||||
},
|
||||
'/session/:sessionId/appium/device/pull_folder': {
|
||||
POST: {command: 'pullFolder', payloadParams: {required: ['path']}},
|
||||
},
|
||||
'/session/:sessionId/appium/settings': {
|
||||
POST: {command: 'updateSettings', payloadParams: {required: ['settings']}},
|
||||
GET: {command: 'getSettings'},
|
||||
},
|
||||
'/session/:sessionId/appium/events': {
|
||||
POST: {command: 'getLogEvents', payloadParams: {optional: ['type']}},
|
||||
},
|
||||
'/session/:sessionId/appium/log_event': {
|
||||
POST: {
|
||||
command: 'logCustomEvent',
|
||||
payloadParams: {required: ['vendor', 'event']},
|
||||
},
|
||||
},
|
||||
// #region Inspector
|
||||
'/session/:sessionId/appium/commands': {
|
||||
GET: {command: 'listCommands'},
|
||||
},
|
||||
'/session/:sessionId/appium/extensions': {
|
||||
GET: {command: 'listExtensions'},
|
||||
},
|
||||
// #endregion
|
||||
|
||||
//
|
||||
// 3rd party vendor/protcol support
|
||||
//
|
||||
// #region Selenium/Chromium browsers
|
||||
'/session/:sessionId/se/log': {
|
||||
POST: {command: 'getLog', payloadParams: {required: ['type']}},
|
||||
},
|
||||
'/session/:sessionId/se/log/types': {
|
||||
GET: {command: 'getLogTypes'},
|
||||
},
|
||||
// #endregion
|
||||
|
||||
// #region chromium devtools
|
||||
// https://chromium.googlesource.com/chromium/src/+/master/chrome/test/chromedriver/server/http_handler.cc
|
||||
'/session/:sessionId/:vendor/cdp/execute': {
|
||||
POST: {command: 'executeCdp', payloadParams: {required: ['cmd', 'params']}},
|
||||
},
|
||||
// #endregion
|
||||
|
||||
// #region Webauthn
|
||||
// https://www.w3.org/TR/webauthn-2/#sctn-automation-add-virtual-authenticator
|
||||
'/session/:sessionId/webauthn/authenticator': {
|
||||
POST: {
|
||||
command: 'addVirtualAuthenticator',
|
||||
payloadParams: {
|
||||
required: ['protocol', 'transport'],
|
||||
optional: ['hasResidentKey', 'hasUserVerification', 'isUserConsenting', 'isUserVerified'],
|
||||
},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/webauthn/authenticator/:authenticatorId': {
|
||||
DELETE: {
|
||||
command: 'removeVirtualAuthenticator',
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credential': {
|
||||
POST: {
|
||||
command: 'addAuthCredential',
|
||||
payloadParams: {
|
||||
required: ['credentialId', 'isResidentCredential', 'rpId', 'privateKey'],
|
||||
optional: ['userHandle', 'signCount'],
|
||||
},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials': {
|
||||
GET: {command: 'getAuthCredential'},
|
||||
DELETE: {command: 'removeAllAuthCredentials'},
|
||||
},
|
||||
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials/:credentialId': {
|
||||
DELETE: {command: 'removeAuthCredential'},
|
||||
},
|
||||
'/session/:sessionId/webauthn/authenticator/:authenticatorId/uv': {
|
||||
POST: {
|
||||
command: 'setUserAuthVerified',
|
||||
payloadParams: {
|
||||
required: ['isUserVerified'],
|
||||
},
|
||||
},
|
||||
},
|
||||
// #endregion
|
||||
|
||||
//
|
||||
// Endpoints deprecated entirely
|
||||
//
|
||||
// #region MJSONWP
|
||||
'/sessions': {
|
||||
GET: {command: 'getSessions', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/timeouts/async_script': {
|
||||
POST: {command: 'asyncScriptTimeout', payloadParams: {required: ['ms']}, deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/timeouts/implicit_wait': {
|
||||
POST: {command: 'implicitWait', payloadParams: {required: ['ms']}, deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/window_handle': {
|
||||
GET: {command: 'getWindowHandle', deprecated: true},
|
||||
},
|
||||
// Only 'window/handles' exists in W3C WebDriver spec.
|
||||
'/session/:sessionId/window/handle': {
|
||||
GET: {command: 'getWindowHandle', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/window_handles': {
|
||||
GET: {command: 'getWindowHandles', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/execute': {
|
||||
POST: {
|
||||
command: 'execute',
|
||||
payloadParams: {required: ['script', 'args']},
|
||||
deprecated: true
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/execute_async': {
|
||||
POST: {
|
||||
command: 'executeAsync',
|
||||
payloadParams: {required: ['script', 'args']},
|
||||
deprecated: true
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/window/:windowhandle/size': {
|
||||
GET: {command: 'getWindowSize', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/window/:windowhandle/position': {
|
||||
POST: {deprecated: true},
|
||||
GET: {deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/window/:windowhandle/maximize': {
|
||||
POST: {command: 'maximizeWindow', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/submit': {
|
||||
POST: {command: 'submit', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/keys': {
|
||||
POST: {command: 'keys', payloadParams: {required: ['value']}, deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/equals/:otherId': {
|
||||
GET: {command: 'equalsElement', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/location': {
|
||||
GET: {command: 'getLocation', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/location_in_view': {
|
||||
GET: {command: 'getLocationInView', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/size': {
|
||||
GET: {command: 'getSize', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/moveto': {
|
||||
POST: {
|
||||
command: 'moveTo',
|
||||
@@ -355,11 +625,7 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
POST: {deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/touch/doubleclick': {
|
||||
POST: {},
|
||||
},
|
||||
'/session/:sessionId/actions': {
|
||||
POST: {command: 'performActions', payloadParams: {required: ['actions']}},
|
||||
DELETE: {command: 'releaseActions'},
|
||||
POST: {deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/touch/longclick': {
|
||||
POST: {
|
||||
@@ -377,10 +643,6 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
deprecated: true,
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/location': {
|
||||
GET: {command: 'getGeoLocation'},
|
||||
POST: {command: 'setGeoLocation', payloadParams: {required: ['location']}},
|
||||
},
|
||||
'/session/:sessionId/local_storage': {
|
||||
GET: {deprecated: true},
|
||||
POST: {deprecated: true},
|
||||
@@ -405,46 +667,32 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
'/session/:sessionId/session_storage/size': {
|
||||
GET: {deprecated: true},
|
||||
},
|
||||
// Selenium 4 clients
|
||||
'/session/:sessionId/se/log': {
|
||||
POST: {command: 'getLog', payloadParams: {required: ['type']}},
|
||||
},
|
||||
// Selenium 4 clients
|
||||
'/session/:sessionId/se/log/types': {
|
||||
GET: {command: 'getLogTypes'},
|
||||
},
|
||||
// mjsonwire, appium clients
|
||||
'/session/:sessionId/log': {
|
||||
POST: {command: 'getLog', payloadParams: {required: ['type']}},
|
||||
},
|
||||
// mjsonwire, appium clients
|
||||
'/session/:sessionId/log/types': {
|
||||
GET: {command: 'getLogTypes'},
|
||||
},
|
||||
'/session/:sessionId/application_cache/status': {
|
||||
GET: {},
|
||||
GET: {deprecated: true},
|
||||
},
|
||||
|
||||
//
|
||||
// mjsonwire
|
||||
//
|
||||
'/session/:sessionId/context': {
|
||||
GET: {command: 'getCurrentContext'},
|
||||
POST: {command: 'setContext', payloadParams: {required: ['name']}},
|
||||
'/session/:sessionId/alert_text': {
|
||||
GET: {command: 'getAlertText', deprecated: true},
|
||||
POST: {
|
||||
command: 'setAlertText',
|
||||
payloadParams: SET_ALERT_TEXT_PAYLOAD_PARAMS,
|
||||
deprecated: true
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/contexts': {
|
||||
GET: {command: 'getContexts'},
|
||||
'/session/:sessionId/accept_alert': {
|
||||
POST: {command: 'postAcceptAlert', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/dismiss_alert': {
|
||||
POST: {command: 'postDismissAlert', deprecated: true},
|
||||
},
|
||||
// Pre-W3C endpoint for element screenshot
|
||||
'/session/:sessionId/screenshot/:elementId': {
|
||||
GET: {command: 'getElementScreenshot', deprecated: true},
|
||||
},
|
||||
// #endregion
|
||||
// #region Appium specific
|
||||
'/session/:sessionId/element/:elementId/pageIndex': {
|
||||
GET: {command: 'getPageIndex', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/network_connection': {
|
||||
GET: {command: 'getNetworkConnection'},
|
||||
POST: {
|
||||
command: 'setNetworkConnection',
|
||||
payloadParams: {unwrap: 'parameters', required: ['type']},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/touch/perform': {
|
||||
POST: {
|
||||
command: 'performTouch',
|
||||
@@ -459,19 +707,9 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
deprecated: true,
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/receive_async_response': {
|
||||
POST: {
|
||||
command: 'receiveAsyncResponse',
|
||||
payloadParams: {required: ['status', 'value']},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/appium/device/shake': {
|
||||
POST: {command: 'mobileShake', deprecated: true},
|
||||
},
|
||||
'/session/:sessionId/appium/device/system_time': {
|
||||
GET: {command: 'getDeviceTime', payloadParams: {optional: ['format']}},
|
||||
POST: {command: 'getDeviceTime', payloadParams: {optional: ['format']}},
|
||||
},
|
||||
'/session/:sessionId/appium/device/lock': {
|
||||
POST: {command: 'lock', payloadParams: {optional: ['seconds']}, deprecated: true},
|
||||
},
|
||||
@@ -575,85 +813,6 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
'/session/:sessionId/appium/device/current_package': {
|
||||
GET: {command: 'getCurrentPackage', deprecated: true},
|
||||
},
|
||||
//region Applications Management
|
||||
'/session/:sessionId/appium/device/install_app': {
|
||||
POST: {
|
||||
command: 'installApp',
|
||||
payloadParams: {
|
||||
required: ['appPath'],
|
||||
optional: ['options'],
|
||||
},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/appium/device/activate_app': {
|
||||
POST: {
|
||||
command: 'activateApp',
|
||||
payloadParams: {
|
||||
required: [['appId'], ['bundleId']],
|
||||
optional: ['options'],
|
||||
},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/appium/device/remove_app': {
|
||||
POST: {
|
||||
command: 'removeApp',
|
||||
payloadParams: {
|
||||
required: [['appId'], ['bundleId']],
|
||||
optional: ['options'],
|
||||
},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/appium/device/terminate_app': {
|
||||
POST: {
|
||||
command: 'terminateApp',
|
||||
payloadParams: {
|
||||
required: [['appId'], ['bundleId']],
|
||||
optional: ['options'],
|
||||
},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/appium/device/app_installed': {
|
||||
POST: {
|
||||
command: 'isAppInstalled',
|
||||
payloadParams: {
|
||||
required: [['appId'], ['bundleId']],
|
||||
},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/appium/device/app_state': {
|
||||
GET: {
|
||||
command: 'queryAppState',
|
||||
payloadParams: {
|
||||
required: [['appId'], ['bundleId']],
|
||||
},
|
||||
},
|
||||
POST: {
|
||||
command: 'queryAppState',
|
||||
payloadParams: {
|
||||
required: [['appId'], ['bundleId']],
|
||||
},
|
||||
deprecated: true,
|
||||
},
|
||||
},
|
||||
//endregion
|
||||
'/session/:sessionId/appium/device/hide_keyboard': {
|
||||
POST: {
|
||||
command: 'hideKeyboard',
|
||||
payloadParams: {optional: ['strategy', 'key', 'keyCode', 'keyName']},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/appium/device/is_keyboard_shown': {
|
||||
GET: {command: 'isKeyboardShown'},
|
||||
},
|
||||
'/session/:sessionId/appium/device/push_file': {
|
||||
POST: {command: 'pushFile', payloadParams: {required: ['path', 'data']}},
|
||||
},
|
||||
'/session/:sessionId/appium/device/pull_file': {
|
||||
POST: {command: 'pullFile', payloadParams: {required: ['path']}},
|
||||
},
|
||||
'/session/:sessionId/appium/device/pull_folder': {
|
||||
POST: {command: 'pullFolder', payloadParams: {required: ['path']}},
|
||||
},
|
||||
'/session/:sessionId/appium/device/toggle_airplane_mode': {
|
||||
POST: {command: 'toggleFlightMode', deprecated: true},
|
||||
},
|
||||
@@ -743,10 +902,6 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
deprecated: true,
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/appium/settings': {
|
||||
POST: {command: 'updateSettings', payloadParams: {required: ['settings']}},
|
||||
GET: {command: 'getSettings'},
|
||||
},
|
||||
'/session/:sessionId/appium/receive_async_response': {
|
||||
POST: {
|
||||
command: 'receiveAsyncResponse',
|
||||
@@ -754,94 +909,6 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
deprecated: true,
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/appium/events': {
|
||||
POST: {command: 'getLogEvents', payloadParams: {optional: ['type']}},
|
||||
},
|
||||
'/session/:sessionId/appium/log_event': {
|
||||
POST: {
|
||||
command: 'logCustomEvent',
|
||||
payloadParams: {required: ['vendor', 'event']},
|
||||
},
|
||||
},
|
||||
|
||||
/*
|
||||
* The W3C spec has some changes to the wire protocol.
|
||||
* https://w3c.github.io/webdriver/webdriver-spec.html
|
||||
* Begin to add those changes here, keeping the old version
|
||||
* since clients still implement them.
|
||||
*/
|
||||
// MJSONWP
|
||||
'/session/:sessionId/alert_text': {
|
||||
GET: {command: 'getAlertText'},
|
||||
POST: {
|
||||
command: 'setAlertText',
|
||||
payloadParams: SET_ALERT_TEXT_PAYLOAD_PARAMS,
|
||||
},
|
||||
},
|
||||
// MJSONWP
|
||||
'/session/:sessionId/accept_alert': {
|
||||
POST: {command: 'postAcceptAlert'},
|
||||
},
|
||||
// MJSONWP
|
||||
'/session/:sessionId/dismiss_alert': {
|
||||
POST: {command: 'postDismissAlert'},
|
||||
},
|
||||
// https://w3c.github.io/webdriver/webdriver-spec.html#user-prompts
|
||||
'/session/:sessionId/alert/text': {
|
||||
GET: {command: 'getAlertText'},
|
||||
POST: {
|
||||
command: 'setAlertText',
|
||||
payloadParams: SET_ALERT_TEXT_PAYLOAD_PARAMS,
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/alert/accept': {
|
||||
POST: {command: 'postAcceptAlert'},
|
||||
},
|
||||
'/session/:sessionId/alert/dismiss': {
|
||||
POST: {command: 'postDismissAlert'},
|
||||
},
|
||||
// https://w3c.github.io/webdriver/webdriver-spec.html#get-element-rect
|
||||
'/session/:sessionId/element/:elementId/rect': {
|
||||
GET: {command: 'getElementRect'},
|
||||
},
|
||||
'/session/:sessionId/execute/sync': {
|
||||
POST: {command: 'execute', payloadParams: {required: ['script', 'args']}},
|
||||
},
|
||||
'/session/:sessionId/execute/async': {
|
||||
POST: {
|
||||
command: 'executeAsync',
|
||||
payloadParams: {required: ['script', 'args']},
|
||||
},
|
||||
},
|
||||
// Pre-W3C endpoint for element screenshot
|
||||
'/session/:sessionId/screenshot/:elementId': {
|
||||
GET: {command: 'getElementScreenshot'},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/screenshot': {
|
||||
GET: {command: 'getElementScreenshot'},
|
||||
},
|
||||
'/session/:sessionId/window/rect': {
|
||||
GET: {command: 'getWindowRect'},
|
||||
POST: {
|
||||
command: 'setWindowRect',
|
||||
payloadParams: {optional: ['x', 'y', 'width', 'height']},
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/window/maximize': {
|
||||
POST: {command: 'maximizeWindow'},
|
||||
},
|
||||
'/session/:sessionId/window/minimize': {
|
||||
POST: {command: 'minimizeWindow'},
|
||||
},
|
||||
'/session/:sessionId/window/fullscreen': {
|
||||
POST: {command: 'fullScreenWindow'},
|
||||
},
|
||||
'/session/:sessionId/window/new': {
|
||||
POST: {command: 'createNewWindow', payloadParams: {optional: ['type']}},
|
||||
},
|
||||
'/session/:sessionId/element/:elementId/property/:name': {
|
||||
GET: {command: 'getProperty'},
|
||||
},
|
||||
'/session/:sessionId/appium/device/set_clipboard': {
|
||||
POST: {
|
||||
command: 'setClipboard',
|
||||
@@ -861,61 +928,15 @@ export const METHOD_MAP = /** @type {const} */ ({
|
||||
deprecated: true,
|
||||
},
|
||||
},
|
||||
|
||||
// chromium devtools
|
||||
// https://chromium.googlesource.com/chromium/src/+/master/chrome/test/chromedriver/server/http_handler.cc
|
||||
'/session/:sessionId/:vendor/cdp/execute': {
|
||||
POST: {command: 'executeCdp', payloadParams: {required: ['cmd', 'params']}},
|
||||
// #endregion
|
||||
// #region JSONWP
|
||||
'/session/:sessionId/log': {
|
||||
POST: {command: 'getLog', payloadParams: {required: ['type']}, deprecated: true},
|
||||
},
|
||||
|
||||
//region Webauthn
|
||||
// https://www.w3.org/TR/webauthn-2/#sctn-automation-add-virtual-authenticator
|
||||
|
||||
'/session/:sessionId/webauthn/authenticator': {
|
||||
POST: {
|
||||
command: 'addVirtualAuthenticator',
|
||||
payloadParams: {
|
||||
required: ['protocol', 'transport'],
|
||||
optional: ['hasResidentKey', 'hasUserVerification', 'isUserConsenting', 'isUserVerified'],
|
||||
},
|
||||
},
|
||||
'/session/:sessionId/log/types': {
|
||||
GET: {command: 'getLogTypes', deprecated: true},
|
||||
},
|
||||
|
||||
'/session/:sessionId/webauthn/authenticator/:authenticatorId': {
|
||||
DELETE: {
|
||||
command: 'removeVirtualAuthenticator',
|
||||
},
|
||||
},
|
||||
|
||||
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credential': {
|
||||
POST: {
|
||||
command: 'addAuthCredential',
|
||||
payloadParams: {
|
||||
required: ['credentialId', 'isResidentCredential', 'rpId', 'privateKey'],
|
||||
optional: ['userHandle', 'signCount'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials': {
|
||||
GET: {command: 'getAuthCredential'},
|
||||
DELETE: {command: 'removeAllAuthCredentials'},
|
||||
},
|
||||
|
||||
'/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials/:credentialId': {
|
||||
DELETE: {command: 'removeAuthCredential'},
|
||||
},
|
||||
|
||||
'/session/:sessionId/webauthn/authenticator/:authenticatorId/uv': {
|
||||
POST: {
|
||||
command: 'setUserAuthVerified',
|
||||
payloadParams: {
|
||||
required: ['isUserVerified'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
//endregion
|
||||
// #endregion
|
||||
});
|
||||
|
||||
// driver command names
|
||||
@@ -926,7 +947,7 @@ export const ALL_COMMANDS = _.flatMap(_.values(METHOD_MAP).map(_.values))
|
||||
/**
|
||||
*
|
||||
* @param {string} endpoint
|
||||
* @param {import('@appium/types').HTTPMethod} method
|
||||
* @param {import('@appium/types').HTTPMethod} [method]
|
||||
* @param {string} [basePath=DEFAULT_BASE_PATH]
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
@@ -944,23 +965,47 @@ export function routeToCommandName(endpoint, method, basePath = DEFAULT_BASE_PAT
|
||||
throw new Error(`'${endpoint}' cannot be translated to a command name: ${err.message}`);
|
||||
}
|
||||
|
||||
const normalizedMethod = _.toUpper(method);
|
||||
const cacheKey = toCommandNameCacheKey(normalizedPathname, normalizedMethod);
|
||||
if (COMMAND_NAMES_CACHE.has(cacheKey)) {
|
||||
return COMMAND_NAMES_CACHE.get(cacheKey) || undefined;
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const possiblePathnames = [];
|
||||
if (!normalizedPathname.startsWith('/session/')) {
|
||||
possiblePathnames.push(`/session/any-session-id${normalizedPathname}`);
|
||||
}
|
||||
possiblePathnames.push(normalizedPathname);
|
||||
const normalizedMethod = _.toUpper(method);
|
||||
for (const [routePath, routeSpec] of _.toPairs(METHOD_MAP)) {
|
||||
const routeMatcher = match(routePath);
|
||||
if (possiblePathnames.some((pp) => routeMatcher(pp))) {
|
||||
const commandName = routeSpec?.[normalizedMethod]?.command;
|
||||
const commandForAnyMethod = () => _.first(
|
||||
(_.keys(routeSpec) ?? []).map((key) => routeSpec[key]?.command)
|
||||
);
|
||||
const commandName = normalizedMethod
|
||||
? routeSpec?.[normalizedMethod]?.command
|
||||
: commandForAnyMethod();
|
||||
if (commandName) {
|
||||
COMMAND_NAMES_CACHE.set(cacheKey, commandName);
|
||||
return commandName;
|
||||
}
|
||||
}
|
||||
}
|
||||
// storing an empty string means we did not find any match for this set of arguments
|
||||
// and we want to cache this result
|
||||
COMMAND_NAMES_CACHE.set(cacheKey, '');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} endpoint
|
||||
* @param {string} [method]
|
||||
* @returns {string}
|
||||
*/
|
||||
function toCommandNameCacheKey(endpoint, method) {
|
||||
return `${endpoint}:${method ?? ''}`;
|
||||
}
|
||||
|
||||
// driver commands that do not require a session to already exist
|
||||
export const NO_SESSION_ID_COMMANDS = ['createSession', 'getStatus', 'getSessions'];
|
||||
export const NO_SESSION_ID_COMMANDS = ['createSession', 'getStatus', 'getSessions', 'getAppiumSessions'];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/base-driver",
|
||||
"version": "9.15.0",
|
||||
"version": "9.16.3",
|
||||
"description": "Base driver class for Appium drivers",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -44,21 +44,16 @@
|
||||
"test:types": "tsd"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^6.0.3",
|
||||
"@appium/types": "^0.24.0",
|
||||
"@appium/support": "^6.0.7",
|
||||
"@appium/types": "^0.25.2",
|
||||
"@colors/colors": "1.6.0",
|
||||
"@types/async-lock": "1.4.2",
|
||||
"@types/bluebird": "3.5.42",
|
||||
"@types/express": "5.0.0",
|
||||
"@types/lodash": "4.17.14",
|
||||
"@types/method-override": "3.0.0",
|
||||
"@types/serve-favicon": "2.5.7",
|
||||
"async-lock": "1.4.1",
|
||||
"asyncbox": "3.0.0",
|
||||
"axios": "1.7.9",
|
||||
"axios": "1.8.2",
|
||||
"bluebird": "3.7.2",
|
||||
"body-parser": "1.20.3",
|
||||
"express": "4.21.2",
|
||||
"fastest-levenshtein": "1.0.16",
|
||||
"http-status-codes": "2.3.0",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "10.4.3",
|
||||
@@ -67,7 +62,7 @@
|
||||
"path-to-regexp": "8.2.0",
|
||||
"serve-favicon": "2.5.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"type-fest": "4.31.0",
|
||||
"type-fest": "4.37.0",
|
||||
"validate.js": "0.13.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
@@ -1,13 +1,63 @@
|
||||
import {BaseDriver} from '../../../lib';
|
||||
import {driverE2ETestSuite} from '@appium/driver-test-support';
|
||||
import { BaseDriver, server, routeConfiguringFunction } from '../../../lib';
|
||||
import { FakeDriver } from '../protocol/fake-driver';
|
||||
import axios from 'axios';
|
||||
import {TEST_HOST, getTestPort, driverE2ETestSuite} from '@appium/driver-test-support';
|
||||
|
||||
const DEFAULT_CAPS = {
|
||||
platformName: 'iOS',
|
||||
'appium:deviceName': 'Delorean',
|
||||
platformName: 'fake',
|
||||
'appium:automationNAme': 'fake',
|
||||
};
|
||||
|
||||
driverE2ETestSuite(BaseDriver, DEFAULT_CAPS);
|
||||
driverE2ETestSuite(BaseDriver, {
|
||||
platformName: 'iOS',
|
||||
'appium:deviceName': 'Delorean',
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {import('@appium/driver-test-support').SessionHelpers} SessionHelpers
|
||||
*/
|
||||
describe('BaseDriver', function () {
|
||||
let port;
|
||||
let baseUrl;
|
||||
|
||||
before(async function () {
|
||||
const chai = await import('chai');
|
||||
const chaisAsPromised = await import('chai-as-promised');
|
||||
chai.use(chaisAsPromised.default);
|
||||
chai.should();
|
||||
|
||||
port = await getTestPort();
|
||||
baseUrl = `http://${TEST_HOST}:${port}`;
|
||||
});
|
||||
|
||||
describe('get appium capabilities', function () {
|
||||
let driver;
|
||||
let sessionId = 'foo';
|
||||
let mjsonwpServer;
|
||||
|
||||
before(async function () {
|
||||
driver = new FakeDriver();
|
||||
driver.sessionId = sessionId;
|
||||
mjsonwpServer = await server({
|
||||
routeConfiguringFunction: routeConfiguringFunction(driver),
|
||||
port,
|
||||
});
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await mjsonwpServer.close();
|
||||
});
|
||||
|
||||
it('should return capabilities', async function () {
|
||||
const capabilities = DEFAULT_CAPS;
|
||||
driver.caps = capabilities;
|
||||
const {data} = await axios({
|
||||
url: `${baseUrl}/session/${sessionId}/appium/capabilities`,
|
||||
method: 'GET',
|
||||
});
|
||||
data.should.eql({
|
||||
value: {
|
||||
capabilities: DEFAULT_CAPS
|
||||
},
|
||||
sessionId
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -153,4 +153,7 @@ describe('parseCapsArray', function () {
|
||||
it('should return an array without change', function () {
|
||||
parseCapsArray(['a', 'b']).should.eql(['a', 'b']);
|
||||
});
|
||||
it('should fail if an invalid JSON array is provided', function () {
|
||||
(() => parseCapsArray(`['*']`)).should.throw();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,9 @@ function buildReqRes(url, method, body) {
|
||||
res.set = (k, v) => {
|
||||
res[k] = v;
|
||||
};
|
||||
res.setHeader = (k, v) => {
|
||||
res.headers[k] = v;
|
||||
};
|
||||
res.status = (code) => {
|
||||
res.sentCode = code;
|
||||
return res;
|
||||
@@ -61,52 +64,86 @@ describe('proxy', function () {
|
||||
});
|
||||
describe('getUrlForProxy', function () {
|
||||
it('should modify session id, host, and port', function () {
|
||||
let j = mockProxy({sessionId: '123'});
|
||||
j.getUrlForProxy('http://host.com:1234/session/456/element/200/value').should.eql(
|
||||
`http://${TEST_HOST}:${port}/session/123/element/200/value`
|
||||
);
|
||||
mockProxy({sessionId: '123'})
|
||||
.getUrlForProxy('http://host.com:1234/session/456/element/200/value', 'POST').should.eql(
|
||||
`http://${TEST_HOST}:${port}/session/123/element/200/value`
|
||||
);
|
||||
});
|
||||
it('should prepend scheme, host and port if not provided', function () {
|
||||
let j = mockProxy({sessionId: '123'});
|
||||
j.getUrlForProxy('/session/456/element/200/value').should.eql(
|
||||
j.getUrlForProxy('/session/456/element/200/value', 'POST').should.eql(
|
||||
`http://${TEST_HOST}:${port}/session/123/element/200/value`
|
||||
);
|
||||
j.getUrlForProxy('/session/456/appium/settings', 'POST').should.eql(
|
||||
`http://${TEST_HOST}:${port}/session/123/appium/settings`
|
||||
);
|
||||
});
|
||||
it('should prepend scheme, host, port and session if not provided', function () {
|
||||
mockProxy({sessionId: '123'})
|
||||
.getUrlForProxy('/element/200/value', 'POST').should.eql(
|
||||
`http://${TEST_HOST}:${port}/session/123/element/200/value`
|
||||
);
|
||||
});
|
||||
it('should keep query parameters', function () {
|
||||
mockProxy({sessionId: '123'})
|
||||
.getUrlForProxy('/element/200/value?foo=1&bar=2', 'POST').should.eql(
|
||||
`http://${TEST_HOST}:${port}/session/123/element/200/value?foo=1&bar=2`
|
||||
);
|
||||
});
|
||||
it('should fix legacy proxy urls if reqBasePath is unset', function () {
|
||||
const j = mockProxy({sessionId: '123', reqBasePath: ''});
|
||||
j.getUrlForProxy('/wd/hub/session/456/element/200/value', 'POST').should.eql(
|
||||
`http://${TEST_HOST}:${port}/session/123/element/200/value`
|
||||
);
|
||||
j.getUrlForProxy('/yolo/session/456/element/200/value', 'POST').should.eql(
|
||||
`http://${TEST_HOST}:${port}/session/123/element/200/value`
|
||||
);
|
||||
});
|
||||
it('should respect nonstandard incoming request base path', function () {
|
||||
let j = mockProxy({sessionId: '123', reqBasePath: ''});
|
||||
j.getUrlForProxy('/session/456/element/200/value').should.eql(
|
||||
`http://${TEST_HOST}:${port}/session/123/element/200/value`
|
||||
);
|
||||
mockProxy({sessionId: '123', reqBasePath: ''})
|
||||
.getUrlForProxy('/session/456/element/200/value', 'POST').should.eql(
|
||||
`http://${TEST_HOST}:${port}/session/123/element/200/value`
|
||||
);
|
||||
|
||||
j = mockProxy({sessionId: '123', reqBasePath: '/my/base/path'});
|
||||
j.getUrlForProxy('/my/base/path/session/456/element/200/value').should.eql(
|
||||
`http://${TEST_HOST}:${port}/session/123/element/200/value`
|
||||
);
|
||||
mockProxy({sessionId: '123', reqBasePath: '/my/base/path'})
|
||||
.getUrlForProxy('/my/base/path/session/456/element/200/value', 'POST').should.eql(
|
||||
`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`
|
||||
);
|
||||
});
|
||||
it('should work with urls which do not have session ids', function () {
|
||||
let j = mockProxy({sessionId: '123'});
|
||||
j.getUrlForProxy('http://host.com:1234/session').should.eql(
|
||||
j.getUrlForProxy('http://host.com:1234/session', 'POST').should.eql(
|
||||
`http://${TEST_HOST}:${port}/session`
|
||||
);
|
||||
|
||||
let newUrl = j.getUrlForProxy('/session');
|
||||
newUrl.should.eql(`http://${TEST_HOST}:${port}/session`);
|
||||
j.getUrlForProxy('/session', 'POST')
|
||||
.should.eql(`http://${TEST_HOST}:${port}/session`);
|
||||
j.getUrlForProxy('/appium/sessions', 'GET')
|
||||
.should.eql(`http://${TEST_HOST}:${port}/appium/sessions`);
|
||||
});
|
||||
it('should throw an error if url requires a sessionId but its null', function () {
|
||||
let j = mockProxy();
|
||||
let e;
|
||||
try {
|
||||
j.getUrlForProxy('/session/456/element/200/value');
|
||||
j.getUrlForProxy('/session/456/element/200/value', 'POST');
|
||||
} catch (err) {
|
||||
e = err;
|
||||
}
|
||||
should.exist(e);
|
||||
e.message.should.contain('without session id');
|
||||
e.message.should.contain('not set');
|
||||
});
|
||||
it('should not throw an error if url does not require a session id and its null', function () {
|
||||
let j = mockProxy();
|
||||
let newUrl = j.getUrlForProxy('/status');
|
||||
|
||||
let newUrl = mockProxy().getUrlForProxy('/status', 'GET');
|
||||
should.exist(newUrl);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('JWProxy', function () {
|
||||
it('should translate host and port', function () {
|
||||
let incomingUrl = PROXY_STATUS_URL;
|
||||
let j = createJWProxy();
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl);
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl, 'GET');
|
||||
proxyUrl.should.equal(testStatusURL);
|
||||
});
|
||||
it('should translate the scheme', function () {
|
||||
@@ -49,21 +49,21 @@ describe('JWProxy', function () {
|
||||
it('should translate the base', function () {
|
||||
let incomingUrl = PROXY_STATUS_URL;
|
||||
let j = createJWProxy({base: ''});
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl);
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl, 'GET');
|
||||
proxyUrl.should.equal(testStatusURL);
|
||||
});
|
||||
it('should translate the session id', function () {
|
||||
let incomingUrl = createProxyURL('foobar', 'element');
|
||||
let j = createJWProxy({sessionId: 'barbaz'});
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl);
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl, 'POST');
|
||||
proxyUrl.should.equal(createTestURL('barbaz', 'element'));
|
||||
});
|
||||
it('should error when translating session commands without session id', function () {
|
||||
let incomingUrl = createProxyURL('foobar', 'element');
|
||||
let j = createJWProxy();
|
||||
(() => {
|
||||
j.getUrlForProxy(incomingUrl);
|
||||
}).should.throw('session id');
|
||||
j.getUrlForProxy(incomingUrl, 'POST');
|
||||
}).should.throw('not set');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('JWProxy', function () {
|
||||
it('should proxy /status', function () {
|
||||
let incomingUrl = '/status';
|
||||
let j = createJWProxy();
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl);
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl, 'GET');
|
||||
proxyUrl.should.equal(testStatusURL);
|
||||
});
|
||||
it('should proxy /session', function () {
|
||||
@@ -83,64 +83,64 @@ describe('JWProxy', function () {
|
||||
it('should proxy /sessions', function () {
|
||||
let incomingUrl = '/sessions';
|
||||
let j = createJWProxy();
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl);
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl, 'GET');
|
||||
proxyUrl.should.equal(createTestURL('', 'sessions'));
|
||||
});
|
||||
it('should proxy session commands based off /session', function () {
|
||||
let incomingUrl = '/session/foobar/element';
|
||||
let j = createJWProxy({sessionId: 'barbaz'});
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl);
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl, 'POST');
|
||||
proxyUrl.should.equal(createTestURL('barbaz', 'element'));
|
||||
});
|
||||
it('should error session commands based off /session without session id', function () {
|
||||
let incomingUrl = '/session/foobar/element';
|
||||
let j = createJWProxy();
|
||||
(() => {
|
||||
j.getUrlForProxy(incomingUrl);
|
||||
}).should.throw('session id');
|
||||
j.getUrlForProxy(incomingUrl, 'POST');
|
||||
}).should.throw('not set');
|
||||
});
|
||||
it('should proxy session commands based off ', function () {
|
||||
let incomingUrl = '/session/3d001db2-7987-42a7-975d-8d5d5304083f/timeouts/implicit_wait';
|
||||
let j = createJWProxy({sessionId: '123'});
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl);
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl, 'POST');
|
||||
proxyUrl.should.equal(createTestURL('123', 'timeouts/implicit_wait'));
|
||||
});
|
||||
it('should proxy session commands based off /session as ""', function () {
|
||||
let incomingUrl = '';
|
||||
let j = createJWProxy();
|
||||
(() => {
|
||||
j.getUrlForProxy(incomingUrl);
|
||||
}).should.throw('session id');
|
||||
j.getUrlForProxy(incomingUrl, 'GET');
|
||||
}).should.throw('not set');
|
||||
j = createJWProxy({sessionId: '123'});
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl);
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl, 'GET');
|
||||
proxyUrl.should.equal(createTestSessionURL('123'));
|
||||
});
|
||||
it('should proxy session commands without /session', function () {
|
||||
let incomingUrl = '/element';
|
||||
let j = createJWProxy({sessionId: 'barbaz'});
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl);
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl, 'POST');
|
||||
proxyUrl.should.equal(createTestURL('barbaz', 'element'));
|
||||
});
|
||||
it(`should proxy session commands when '/session' is in the url`, function () {
|
||||
let incomingUrl =
|
||||
'/session/82a9b7da-faaf-4a1d-8ef3-5e4fb5812200/cookie/session-something-or-other';
|
||||
let j = createJWProxy({sessionId: 'barbaz'});
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl);
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl, 'POST');
|
||||
proxyUrl.should.equal(createTestURL('barbaz', 'cookie/session-something-or-other'));
|
||||
});
|
||||
it(`should proxy session commands when '/session' is in the url and not base on the original url`, function () {
|
||||
let incomingUrl =
|
||||
'/session/82a9b7da-faaf-4a1d-8ef3-5e4fb5812200/cookie/session-something-or-other';
|
||||
let j = createJWProxy({sessionId: 'barbaz'});
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl);
|
||||
let proxyUrl = j.getUrlForProxy(incomingUrl, 'POST');
|
||||
proxyUrl.should.equal(createTestURL('barbaz', 'cookie/session-something-or-other'));
|
||||
});
|
||||
it('should error session commands without /session without session id', function () {
|
||||
let incomingUrl = '/element';
|
||||
let j = createJWProxy();
|
||||
(() => {
|
||||
j.getUrlForProxy(incomingUrl);
|
||||
}).should.throw('session id');
|
||||
j.getUrlForProxy(incomingUrl, 'POST');
|
||||
}).should.throw('not set');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('Protocol', function () {
|
||||
}
|
||||
let hash = shasum.digest('hex').substring(0, 8);
|
||||
// Modify the hash whenever the protocol has intentionally been modified.
|
||||
hash.should.equal('6e93ad90');
|
||||
hash.should.equal('00fcb083');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.2](https://github.com/appium/appium/compare/@appium/base-plugin@2.3.1...@appium/base-plugin@2.3.2) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.1](https://github.com/appium/appium/compare/@appium/base-plugin@2.3.0...@appium/base-plugin@2.3.1) (2025-02-19)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.0](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.52...@appium/base-plugin@2.3.0) (2025-01-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/base-plugin",
|
||||
"version": "2.3.0",
|
||||
"version": "2.3.4",
|
||||
"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.15.0",
|
||||
"@appium/support": "^6.0.3"
|
||||
"@appium/base-driver": "^9.16.3",
|
||||
"@appium/support": "^6.0.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
|
||||
@@ -3,6 +3,46 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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.0.31](https://github.com/appium/appium/compare/@appium/docutils@1.0.30...@appium/docutils@1.0.31) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/docutils
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.30](https://github.com/appium/appium/compare/@appium/docutils@1.0.29...@appium/docutils@1.0.30) (2025-02-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **docutils:** update dependency consola to v3.4.0 ([#20908](https://github.com/appium/appium/issues/20908)) ([8fb0da3](https://github.com/appium/appium/commit/8fb0da34bb58c44c30ac8900a62510f1fcb663bc))
|
||||
* **docutils:** update dependency typescript to v5.7.3 ([#20893](https://github.com/appium/appium/issues/20893)) ([6706a00](https://github.com/appium/appium/commit/6706a0037eda5f7eb9eb2a4b0fb49888618c12c2))
|
||||
* **support:** update dependency semver to v7.7.0 ([#20948](https://github.com/appium/appium/issues/20948)) ([81ebd75](https://github.com/appium/appium/commit/81ebd758cb8a87d388124036e5158207041a6b2f))
|
||||
* **support:** update dependency semver to v7.7.1 ([#20961](https://github.com/appium/appium/issues/20961)) ([4c08944](https://github.com/appium/appium/commit/4c089440e655fbe3c8da919f8f625b7d7179f00a))
|
||||
* **types:** update dependency type-fest to v4.32.0 ([#20900](https://github.com/appium/appium/issues/20900)) ([fbc8530](https://github.com/appium/appium/commit/fbc85308a5398e7c9966792da713e60e47ed7f00))
|
||||
* **types:** update dependency type-fest to v4.33.0 ([#20923](https://github.com/appium/appium/issues/20923)) ([2409d32](https://github.com/appium/appium/commit/2409d3223a77aa7e84e0cb05a70be3bfa0c69157))
|
||||
* **types:** update dependency type-fest to v4.34.1 ([#20971](https://github.com/appium/appium/issues/20971)) ([0a7490e](https://github.com/appium/appium/commit/0a7490ed53ccfa9243df779e74bafadfc8415c87))
|
||||
* **types:** update dependency type-fest to v4.35.0 ([#20999](https://github.com/appium/appium/issues/20999)) ([3dc7336](https://github.com/appium/appium/commit/3dc7336b5fce10b9c1b095cd7a8a1841dbc3de12))
|
||||
|
||||
|
||||
|
||||
## [1.0.29](https://github.com/appium/appium/compare/@appium/docutils@1.0.28...@appium/docutils@1.0.29) (2025-01-08)
|
||||
|
||||
**Note:** Version bump only for package @appium/docutils
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/docutils",
|
||||
"version": "1.0.29",
|
||||
"version": "1.0.33",
|
||||
"description": "Documentation generation utilities for Appium and related projects",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -48,23 +48,22 @@
|
||||
"start": "node ./build/lib/cli/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^6.0.3",
|
||||
"@appium/tsconfig": "^0.3.3",
|
||||
"@appium/support": "^6.0.7",
|
||||
"@appium/tsconfig": "^0.3.5",
|
||||
"@sliphua/lilconfig-ts-loader": "3.2.2",
|
||||
"@types/which": "3.0.4",
|
||||
"chalk": "4.1.2",
|
||||
"consola": "3.3.3",
|
||||
"consola": "3.4.0",
|
||||
"diff": "7.0.0",
|
||||
"json5": "2.2.3",
|
||||
"lilconfig": "3.1.3",
|
||||
"lodash": "4.17.21",
|
||||
"pkg-dir": "5.0.0",
|
||||
"read-pkg": "5.2.0",
|
||||
"semver": "7.6.3",
|
||||
"semver": "7.7.1",
|
||||
"source-map-support": "0.5.21",
|
||||
"teen_process": "2.2.3",
|
||||
"type-fest": "4.31.0",
|
||||
"typescript": "5.7.2",
|
||||
"teen_process": "2.3.1",
|
||||
"type-fest": "4.37.0",
|
||||
"typescript": "5.8.2",
|
||||
"yaml": "2.7.0",
|
||||
"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.5.49
|
||||
mkdocs-git-revision-date-localized-plugin==1.4.4
|
||||
mkdocs-material==9.6.7
|
||||
mkdocs-redirects==1.2.2
|
||||
mike==2.1.3
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.5](https://github.com/appium/appium/compare/@appium/driver-test-support@0.7.4...@appium/driver-test-support@0.7.5) (2025-02-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **driver-test-support:** update definitelytyped ([#20942](https://github.com/appium/appium/issues/20942)) ([f7c63ee](https://github.com/appium/appium/commit/f7c63ee5d87be21eba577a4cfd0b0a08a050afeb))
|
||||
* **driver-test-support:** update dependency chai to v5.2.0 ([#20998](https://github.com/appium/appium/issues/20998)) ([ca456e5](https://github.com/appium/appium/commit/ca456e52365a9e621b4473263cc78bb2a3a855e2))
|
||||
* **types:** update dependency type-fest to v4.32.0 ([#20900](https://github.com/appium/appium/issues/20900)) ([fbc8530](https://github.com/appium/appium/commit/fbc85308a5398e7c9966792da713e60e47ed7f00))
|
||||
* **types:** update dependency type-fest to v4.33.0 ([#20923](https://github.com/appium/appium/issues/20923)) ([2409d32](https://github.com/appium/appium/commit/2409d3223a77aa7e84e0cb05a70be3bfa0c69157))
|
||||
* **types:** update dependency type-fest to v4.34.1 ([#20971](https://github.com/appium/appium/issues/20971)) ([0a7490e](https://github.com/appium/appium/commit/0a7490ed53ccfa9243df779e74bafadfc8415c87))
|
||||
* **types:** update dependency type-fest to v4.35.0 ([#20999](https://github.com/appium/appium/issues/20999)) ([3dc7336](https://github.com/appium/appium/commit/3dc7336b5fce10b9c1b095cd7a8a1841dbc3de12))
|
||||
|
||||
|
||||
|
||||
## [0.7.4](https://github.com/appium/appium/compare/@appium/driver-test-support@0.7.3...@appium/driver-test-support@0.7.4) (2025-01-08)
|
||||
|
||||
**Note:** Version bump only for package @appium/driver-test-support
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/driver-test-support",
|
||||
"version": "0.7.4",
|
||||
"version": "0.7.7",
|
||||
"description": "Test utilities for Appium drivers",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -41,17 +41,16 @@
|
||||
},
|
||||
"types": "./build/lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@appium/types": "^0.24.0",
|
||||
"@types/lodash": "4.17.14",
|
||||
"axios": "1.7.9",
|
||||
"@appium/types": "^0.25.2",
|
||||
"axios": "1.8.2",
|
||||
"bluebird": "3.7.2",
|
||||
"chai": "5.1.2",
|
||||
"chai": "5.2.0",
|
||||
"chai-as-promised": "8.0.1",
|
||||
"get-port": "5.1.1",
|
||||
"lodash": "4.17.21",
|
||||
"sinon": "19.0.2",
|
||||
"source-map-support": "0.5.21",
|
||||
"type-fest": "4.31.0"
|
||||
"type-fest": "4.37.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.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.0.2](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@1.0.1...@appium/eslint-config-appium-ts@1.0.2) (2025-01-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@appium/eslint-config-appium-ts",
|
||||
"type": "module",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"description": "Shared ESLint config for Appium projects (TypeScript version)",
|
||||
"keywords": [
|
||||
"eslint",
|
||||
@@ -32,7 +32,7 @@
|
||||
"@eslint/compat": "^1.1.0",
|
||||
"@eslint/js": "^9.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.0.0 || ^10.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.0",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-mocha": "^10.4.0",
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.5.3 ([#20895](https://github.com/appium/appium/issues/20895)) ([8741474](https://github.com/appium/appium/commit/874147449a26bbec03b08e7e95fb9e4c6b5881af))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.5.4 ([#20898](https://github.com/appium/appium/issues/20898)) ([7f91d5c](https://github.com/appium/appium/commit/7f91d5cba850100124088d774ba8704e3765863f))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.5.7 ([#20903](https://github.com/appium/appium/issues/20903)) ([c60fdc8](https://github.com/appium/appium/commit/c60fdc8d847b4aeed2d8b8307180194ee32e5ca9))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.7.0 ([#20930](https://github.com/appium/appium/issues/20930)) ([5780982](https://github.com/appium/appium/commit/57809826a410b71dca155f59e081f77dcaf671ed))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.7.1 ([#20934](https://github.com/appium/appium/issues/20934)) ([50fa5d6](https://github.com/appium/appium/commit/50fa5d670bd967200a78e5b4b8a54d5c9b85901b))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.7.2 ([#20944](https://github.com/appium/appium/issues/20944)) ([f5e0819](https://github.com/appium/appium/commit/f5e0819f505b55421b7d260f69a41ae980390a91))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.8.0 ([#20964](https://github.com/appium/appium/issues/20964)) ([ce32226](https://github.com/appium/appium/commit/ce32226cb57fb3f07bf4862dbbfcba227ff1230c))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.9.0 ([#20994](https://github.com/appium/appium/issues/20994)) ([b3c877f](https://github.com/appium/appium/commit/b3c877f66ddf3ccf7a25ca1f7a3952c660bd7dda))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.9.1 ([#20996](https://github.com/appium/appium/issues/20996)) ([c6a080f](https://github.com/appium/appium/commit/c6a080f7ade03a49a274ce6fbc84629406fdc2a7))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.9.3 ([#21005](https://github.com/appium/appium/issues/21005)) ([686a630](https://github.com/appium/appium/commit/686a630fbdf525db16b42fdfc1bbdaf6426b41d0))
|
||||
|
||||
|
||||
|
||||
## [4.0.1](https://github.com/appium/appium/compare/@appium/execute-driver-plugin@4.0.0...@appium/execute-driver-plugin@4.0.1) (2025-01-06)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/execute-driver-plugin",
|
||||
"version": "4.0.1",
|
||||
"version": "4.0.3",
|
||||
"description": "Plugin for batching and executing driver commands with Appiums",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -38,11 +38,10 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/bluebird": "3.5.42",
|
||||
"bluebird": "3.7.2",
|
||||
"lodash": "4.17.21",
|
||||
"source-map-support": "0.5.21",
|
||||
"webdriverio": "9.5.1"
|
||||
"webdriverio": "9.10.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"appium": "^2.0.0-beta.35"
|
||||
|
||||
@@ -3,6 +3,23 @@
|
||||
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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **universal-xml-plugin:** update dependency @xmldom/xmldom to v0.9.7 ([#20918](https://github.com/appium/appium/issues/20918)) ([7e1f1e5](https://github.com/appium/appium/commit/7e1f1e5aad19d709a48cd4d06196fa4db792400f))
|
||||
|
||||
|
||||
|
||||
## [5.7.0](https://github.com/appium/appium/compare/@appium/fake-driver@5.6.0...@appium/fake-driver@5.7.0) (2025-01-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/fake-driver",
|
||||
"version": "5.7.0",
|
||||
"version": "5.7.2",
|
||||
"description": "Mock driver used internally by Appium for testing. Ignore",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -42,8 +42,7 @@
|
||||
"test:unit": "mocha --timeout 10s --slow 5s \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/bluebird": "3.5.42",
|
||||
"@xmldom/xmldom": "0.9.6",
|
||||
"@xmldom/xmldom": "0.9.8",
|
||||
"asyncbox": "3.0.0",
|
||||
"bluebird": "3.7.2",
|
||||
"lodash": "4.17.21",
|
||||
@@ -51,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,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.2.2](https://github.com/appium/appium/compare/@appium/fake-plugin@3.2.1...@appium/fake-plugin@3.2.2) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.2.1](https://github.com/appium/appium/compare/@appium/fake-plugin@3.2.0...@appium/fake-plugin@3.2.1) (2025-02-19)
|
||||
|
||||
**Note:** Version bump only for package @appium/fake-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.2.0](https://github.com/appium/appium/compare/@appium/fake-plugin@3.1.56...@appium/fake-plugin@3.2.0) (2025-01-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/fake-plugin",
|
||||
"version": "3.2.0",
|
||||
"version": "3.2.4",
|
||||
"description": "A fake Appium 2.0 plugin",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -39,9 +39,8 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-plugin": "^2.3.0",
|
||||
"@appium/support": "^6.0.3",
|
||||
"@types/bluebird": "3.5.42",
|
||||
"@appium/base-plugin": "^2.3.4",
|
||||
"@appium/support": "^6.0.7",
|
||||
"bluebird": "3.7.2",
|
||||
"lodash": "4.17.21",
|
||||
"source-map-support": "0.5.21"
|
||||
|
||||
@@ -3,6 +3,39 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.0.29](https://github.com/appium/appium/compare/@appium/images-plugin@3.0.28...@appium/images-plugin@3.0.29) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/images-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.0.28](https://github.com/appium/appium/compare/@appium/images-plugin@3.0.27...@appium/images-plugin@3.0.28) (2025-02-19)
|
||||
|
||||
**Note:** Version bump only for package @appium/images-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.0.27](https://github.com/appium/appium/compare/@appium/images-plugin@3.0.26...@appium/images-plugin@3.0.27) (2025-01-08)
|
||||
|
||||
**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.27",
|
||||
"version": "3.1.0",
|
||||
"description": "Plugin for working with images and image elements in Appium",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -34,13 +34,13 @@
|
||||
],
|
||||
"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.7",
|
||||
"@appium/support": "^6.0.3",
|
||||
"@appium/opencv": "^3.0.8",
|
||||
"@appium/support": "^6.0.7",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "10.4.3",
|
||||
"sharp": "0.33.5",
|
||||
|
||||
+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,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.0.7](https://github.com/appium/appium/compare/@appium/opencv@3.0.6...@appium/opencv@3.0.7) (2025-01-02)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/opencv",
|
||||
"version": "3.0.7",
|
||||
"version": "3.0.8",
|
||||
"description": "OpenCV-related helper library",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -41,7 +41,6 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/bluebird": "3.5.42",
|
||||
"bluebird": "3.7.2",
|
||||
"lodash": "4.17.21",
|
||||
"opencv-bindings": "4.5.5",
|
||||
|
||||
@@ -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.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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.50](https://github.com/appium/appium/compare/@appium/plugin-test-support@0.3.49...@appium/plugin-test-support@0.3.50) (2025-02-19)
|
||||
|
||||
**Note:** Version bump only for package @appium/plugin-test-support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.49](https://github.com/appium/appium/compare/@appium/plugin-test-support@0.3.48...@appium/plugin-test-support@0.3.49) (2025-01-08)
|
||||
|
||||
**Note:** Version bump only for package @appium/plugin-test-support
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/plugin-test-support",
|
||||
"version": "0.3.49",
|
||||
"version": "0.3.52",
|
||||
"description": "Testing utilities for Appium plugins",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -41,11 +41,11 @@
|
||||
"test:unit": "exit 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/types": "^0.24.0",
|
||||
"@appium/types": "^0.25.2",
|
||||
"get-port": "5.1.1",
|
||||
"log-symbols": "4.1.0",
|
||||
"source-map-support": "0.5.21",
|
||||
"teen_process": "2.2.3"
|
||||
"teen_process": "2.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"appium": "^2.0.0-beta.43",
|
||||
|
||||
@@ -3,6 +3,23 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.8.1](https://github.com/appium/appium/compare/@appium/schema@0.8.0...@appium/schema@0.8.1) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/schema
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.8.0](https://github.com/appium/appium/compare/@appium/schema@0.7.1...@appium/schema@0.8.0) (2025-02-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **appium:** Add a command line parameter to configure HTTP server request timeout ([#21003](https://github.com/appium/appium/issues/21003)) ([eb1b156](https://github.com/appium/appium/commit/eb1b156146bc338da9c6ded5a2c5beab22ac0ed8))
|
||||
|
||||
|
||||
|
||||
## [0.7.1](https://github.com/appium/appium/compare/@appium/schema@0.7.0...@appium/schema@0.7.1) (2025-01-02)
|
||||
|
||||
|
||||
|
||||
@@ -117,11 +117,23 @@ export const AppiumConfigJsonSchema = /** @type {const} */ ({
|
||||
appiumCliAliases: ['ka'],
|
||||
default: 600,
|
||||
description:
|
||||
'Number of seconds the Appium server should apply as both the keep-alive timeout and the connection timeout for all requests. A value of 0 disables the timeout.',
|
||||
'Number of seconds the Appium server should apply as both the keep-alive timeout and the connection timeout ' +
|
||||
'for all requests. A value of 0 disables the timeout.',
|
||||
minimum: 0,
|
||||
title: 'keep-alive-timeout config',
|
||||
type: 'integer',
|
||||
},
|
||||
'request-timeout': {
|
||||
default: 3600,
|
||||
description:
|
||||
'Number of seconds the Appium server should apply for receiving the entire HTTP request from the client. ' +
|
||||
'A value of 0 disables the timeout. Set it to a non-zero value to protect against ' +
|
||||
'potential Denial-of-Service attacks in case the server is deployed without a reverse proxy in front. ' +
|
||||
'HTTP requests that are running longer than allowed by this timeout would be rejected with the status code 408.',
|
||||
minimum: 0,
|
||||
title: 'request-timeout config',
|
||||
type: 'integer',
|
||||
},
|
||||
'local-timezone': {
|
||||
default: false,
|
||||
description: 'Use local timezone for timestamps',
|
||||
|
||||
@@ -120,6 +120,13 @@
|
||||
"title": "keep-alive-timeout config",
|
||||
"type": "integer"
|
||||
},
|
||||
"request-timeout": {
|
||||
"default": 3600,
|
||||
"description": "Number of seconds the Appium server should apply for receiving the entire HTTP request from the client. A value of 0 disables the timeout. Set it to a non-zero value to protect against potential Denial-of-Service attacks in case the server is deployed without a reverse proxy in front. HTTP requests that are running longer than allowed by this timeout would be rejected with the status code 408.",
|
||||
"minimum": 0,
|
||||
"title": "request-timeout config",
|
||||
"type": "integer"
|
||||
},
|
||||
"local-timezone": {
|
||||
"default": false,
|
||||
"description": "Use local timezone for timestamps",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/schema",
|
||||
"version": "0.7.1",
|
||||
"version": "0.8.1",
|
||||
"description": "Appium Configuration Schema",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -19,7 +19,7 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appium/appium.git",
|
||||
"directory": "schema"
|
||||
"directory": "packages/schema"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "https://github.com/appium",
|
||||
@@ -38,7 +38,6 @@
|
||||
"test": "exit 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/json-schema": "7.0.15",
|
||||
"json-schema": "0.4.0",
|
||||
"source-map-support": "0.5.21"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# 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.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 { toW3cResponseError } from './utils';
|
||||
import { AppiumServer } from '@appium/types';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
import WebSocket from 'ws';
|
||||
import { EventEmitter } from 'node:stream';
|
||||
|
||||
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,14 @@
|
||||
import _ from 'lodash';
|
||||
import { errors } from 'appium/driver';
|
||||
|
||||
|
||||
export function toW3cResponseError(err: any): [number, Record<string, any>] {
|
||||
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,71 @@
|
||||
{
|
||||
"name": "@appium/storage-plugin",
|
||||
"version": "0.1.0",
|
||||
"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/base-plugin": "^2.3.4",
|
||||
"@appium/support": "^6.0.7",
|
||||
"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,47 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [6.0.5](https://github.com/appium/appium/compare/@appium/support@6.0.4...@appium/support@6.0.5) (2025-02-20)
|
||||
|
||||
**Note:** Version bump only for package @appium/support
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [6.0.4](https://github.com/appium/appium/compare/@appium/support@6.0.3...@appium/support@6.0.4) (2025-02-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Avoid "Unhandled rejection" error on mjpeg connection failure ([#20909](https://github.com/appium/appium/issues/20909)) ([3c58054](https://github.com/appium/appium/commit/3c58054a476fbb92927ffa54732d1995e1b956c3))
|
||||
* **support:** update dependency form-data to v4.0.2 ([#20997](https://github.com/appium/appium/issues/20997)) ([224f524](https://github.com/appium/appium/commit/224f5247b0cd4b7dcc392567b90d53b9b57a8b88))
|
||||
* **support:** update dependency semver to v7.7.0 ([#20948](https://github.com/appium/appium/issues/20948)) ([81ebd75](https://github.com/appium/appium/commit/81ebd758cb8a87d388124036e5158207041a6b2f))
|
||||
* **support:** update dependency semver to v7.7.1 ([#20961](https://github.com/appium/appium/issues/20961)) ([4c08944](https://github.com/appium/appium/commit/4c089440e655fbe3c8da919f8f625b7d7179f00a))
|
||||
* **support:** update dependency uuid to v11.0.5 ([#20896](https://github.com/appium/appium/issues/20896)) ([561aff6](https://github.com/appium/appium/commit/561aff641a4f62c82865ea036b6e1ca1bb89fb44))
|
||||
* **types:** update dependency type-fest to v4.32.0 ([#20900](https://github.com/appium/appium/issues/20900)) ([fbc8530](https://github.com/appium/appium/commit/fbc85308a5398e7c9966792da713e60e47ed7f00))
|
||||
* **types:** update dependency type-fest to v4.33.0 ([#20923](https://github.com/appium/appium/issues/20923)) ([2409d32](https://github.com/appium/appium/commit/2409d3223a77aa7e84e0cb05a70be3bfa0c69157))
|
||||
* **types:** update dependency type-fest to v4.34.1 ([#20971](https://github.com/appium/appium/issues/20971)) ([0a7490e](https://github.com/appium/appium/commit/0a7490ed53ccfa9243df779e74bafadfc8415c87))
|
||||
* **types:** update dependency type-fest to v4.35.0 ([#20999](https://github.com/appium/appium/issues/20999)) ([3dc7336](https://github.com/appium/appium/commit/3dc7336b5fce10b9c1b095cd7a8a1841dbc3de12))
|
||||
|
||||
|
||||
|
||||
## [6.0.3](https://github.com/appium/appium/compare/@appium/support@6.0.2...@appium/support@6.0.3) (2025-01-08)
|
||||
|
||||
**Note:** Version bump only for package @appium/support
|
||||
|
||||
@@ -116,22 +116,23 @@ class MJpegStream extends Writable {
|
||||
await initMJpegConsumer();
|
||||
|
||||
this.consumer = new MJpegConsumer();
|
||||
|
||||
// use the deferred pattern so we can wait for the start of the stream
|
||||
// based on what comes in from an external pipe
|
||||
const startPromise = new B((res, rej) => {
|
||||
this.registerStartSuccess = res;
|
||||
this.registerStartFailure = rej;
|
||||
})
|
||||
// start a timeout so that if the server does not return data, we don't
|
||||
// block forever.
|
||||
.timeout(
|
||||
serverTimeout,
|
||||
`Waited ${serverTimeout}ms but the MJPEG server never sent any images`
|
||||
);
|
||||
|
||||
const url = this.url;
|
||||
const onErr = (err) => {
|
||||
try {
|
||||
this.responseStream = (
|
||||
await axios({
|
||||
url,
|
||||
responseType: 'stream',
|
||||
timeout: serverTimeout,
|
||||
})
|
||||
).data;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Cannot connect to the MJPEG stream at ${url}. ` +
|
||||
`Original error: ${_.has(e, 'response') ? JSON.stringify(e.response) : /** @type {Error} */ (e).message}`
|
||||
);
|
||||
}
|
||||
|
||||
const onErr = (/** @type {Error} */ err) => {
|
||||
// Make sure we don't get an outdated screenshot if there was an error
|
||||
this.lastChunk = null;
|
||||
|
||||
@@ -145,18 +146,18 @@ class MJpegStream extends Writable {
|
||||
log.debug(`The connection to MJPEG server at ${url} has been closed`);
|
||||
this.lastChunk = null;
|
||||
};
|
||||
|
||||
try {
|
||||
this.responseStream = (
|
||||
await axios({
|
||||
url,
|
||||
responseType: 'stream',
|
||||
timeout: serverTimeout,
|
||||
})
|
||||
).data;
|
||||
} catch (e) {
|
||||
return onErr(e);
|
||||
}
|
||||
// use the deferred pattern so we can wait for the start of the stream
|
||||
// based on what comes in from an external pipe
|
||||
const startPromise = new B((res, rej) => {
|
||||
this.registerStartSuccess = res;
|
||||
this.registerStartFailure = rej;
|
||||
})
|
||||
// start a timeout so that if the server does not return data, we don't
|
||||
// block forever.
|
||||
.timeout(
|
||||
serverTimeout,
|
||||
`Waited ${serverTimeout}ms but the MJPEG server never sent any images`
|
||||
);
|
||||
|
||||
this.responseStream
|
||||
.once('close', onClose)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user