mirror of
https://github.com/appium/appium.git
synced 2026-11-08 19:06:50 +08:00
Compare commits
126
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
a7ce83e169 | ||
|
|
8df1c217a1 | ||
|
|
aad5709913 | ||
|
|
17ecc21747 | ||
|
|
3467f32c8e | ||
|
|
6f27d36cf5 | ||
|
|
488e5dea82 | ||
|
|
13d8b03cc1 | ||
|
|
fe209ed660 | ||
|
|
93e4288253 | ||
|
|
fb839be6ea | ||
|
|
6742808345 | ||
|
|
bb4ba0e52b | ||
|
|
990a0667e3 | ||
|
|
389a3e83ac | ||
|
|
a034880a03 | ||
|
|
588381ecf4 | ||
|
|
3fc57a15b5 | ||
|
|
bbfc04026f | ||
|
|
c5443e9fc9 | ||
|
|
31c6759061 | ||
|
|
edd1c05d1e | ||
|
|
61b6689897 | ||
|
|
65658ccbdd | ||
|
|
25ea9f73bc | ||
|
|
4b3a8c2197 | ||
|
|
24abb385e5 | ||
|
|
861d060713 | ||
|
|
c57f81d134 | ||
|
|
51acf54fe5 | ||
|
|
aa880837da | ||
|
|
af7aba492d | ||
|
|
25b8483294 | ||
|
|
4474412a60 | ||
|
|
7c740f9b3a | ||
|
|
c765bc31e0 | ||
|
|
7abecaddd3 | ||
|
|
083f7e2328 | ||
|
|
aaef1d6962 | ||
|
|
64e768efb7 | ||
|
|
a5897dd25a | ||
|
|
66e3580731 | ||
|
|
16e9f94903 | ||
|
|
a917ec6ced | ||
|
|
342d6641ee | ||
|
|
04e2d1154b | ||
|
|
c032352fad | ||
|
|
e3774ef0c0 |
@@ -1,9 +0,0 @@
|
||||
**/coverage/**
|
||||
**/node_modules/**
|
||||
examples/javascript-wd
|
||||
**/build/**
|
||||
**/*.min.js
|
||||
sample-code
|
||||
**/build-fixtures/**
|
||||
packages/appium/docs/**/assets/**
|
||||
packages/appium/docs/**/js/**
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"extends": ["@appium/eslint-config-appium-ts"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "packages/fake-driver/**/*",
|
||||
"rules": {"require-await": "off"}
|
||||
},
|
||||
{"files": "packages/support/**/*", "globals": {"BigInt": "readonly"}},
|
||||
{
|
||||
"files": "packages/*/test/**/*",
|
||||
"rules": {"func-names": "off"}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"packages/appium/support.js",
|
||||
"packages/appium/driver.js",
|
||||
"packages/appium/plugin.js"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "script"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"./test/setup.js",
|
||||
"./**/scripts/**/*.js",
|
||||
"./packages/*/index.js",
|
||||
"./packages/docutils/bin/appium-docs.js"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-var-requires": "off"
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "script"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "./packages/*/test/**/*.js",
|
||||
"rules": {
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"no-restricted-properties": [
|
||||
"error",
|
||||
{
|
||||
"object": "sinon",
|
||||
"property": "spy",
|
||||
"message": "Use `sandbox = sinon.createSandbox()` and `sandbox.spy()` instead. Don't forget to call `sandbox.restore()` in `afterEach`"
|
||||
},
|
||||
{
|
||||
"object": "sinon",
|
||||
"property": "stub",
|
||||
"message": "Use `sandbox = sinon.createSandbox()` and `sandbox.stub()` instead. Don't forget to call `sandbox.restore()` in `afterEach`"
|
||||
},
|
||||
{
|
||||
"object": "sinon",
|
||||
"property": "mock",
|
||||
"message": "Use `sandbox = sinon.createSandbox()` and `sandbox.mock()` instead. Don't forget to call `sandbox.restore()` in `afterEach`"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -24,7 +24,7 @@ targets:
|
||||
- type: npm
|
||||
path: ./packages/base-driver
|
||||
- type: npm
|
||||
path: ./packages/eslint-config-appium
|
||||
path: ./packages/eslint-config-appium-ts
|
||||
- type: npm
|
||||
path: ./packages/fake-driver
|
||||
- type: npm
|
||||
|
||||
@@ -63,11 +63,6 @@ labels:
|
||||
matcher:
|
||||
files: ['packages/driver-test-support/**']
|
||||
|
||||
- label: '@appium/eslint-config-appium'
|
||||
sync: true
|
||||
matcher:
|
||||
files: ['packages/eslint-config-appium/**']
|
||||
|
||||
- label: '@appium/eslint-config-appium-ts'
|
||||
sync: true
|
||||
matcher:
|
||||
|
||||
@@ -42,6 +42,9 @@ on:
|
||||
- '!**/sample-code/**'
|
||||
- '!packages/*/docs/**'
|
||||
|
||||
env:
|
||||
CI: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
||||
@@ -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.5
|
||||
uses: peter-evans/create-pull-request@v7.0.6
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
commit-message: 'docs(appium): Update documentation translations'
|
||||
|
||||
+1
-1
@@ -23,7 +23,6 @@ to driver/client documentation
|
||||
* [base-plugin](packages/base-plugin/CHANGELOG.md)
|
||||
* [docutils](packages/docutils/CHANGELOG.md)
|
||||
* [driver-test-support](packages/driver-test-support/CHANGELOG.md)
|
||||
* [eslint-config-appium](packages/eslint-config-appium/CHANGELOG.md)
|
||||
* [eslint-config-appium-ts](packages/eslint-config-appium-ts/CHANGELOG.md)
|
||||
* [fake-driver](packages/fake-driver/CHANGELOG.md)
|
||||
* [fake-plugin](packages/fake-plugin/CHANGELOG.md)
|
||||
@@ -40,5 +39,6 @@ to driver/client documentation
|
||||
## Removed Modules
|
||||
The changelog links used for these modules lead to the last version of their changelog file.
|
||||
* [doctor](https://github.com/appium/appium/blob/8daf5e123ac14c8325acf504fb33eb28e1a3dd78/packages/doctor/CHANGELOG.md) ([removal PR](https://github.com/appium/appium/pull/20805))
|
||||
* [eslint-config-appium](https://github.com/appium/appium/blob/4b3a8c2197a587ec5a2606b662e4e7667a7852cb/packages/eslint-config-appium/CHANGELOG.md) ([removal PR](https://github.com/appium/appium/pull/20856))
|
||||
* [gulp-plugins](https://github.com/appium/appium/blob/0823f0b60e40395cd1dc3b72cfa3c0092bc81302/packages/gulp-plugins/CHANGELOG.md) ([removal PR](https://github.com/appium/appium/pull/17943))
|
||||
* [typedoc-plugin-appium](https://github.com/appium/appium/blob/d6204b6902074210943d7bbbf72d139b9b170a20/packages/typedoc-plugin-appium/CHANGELOG.md) ([removal PR](https://github.com/appium/appium/pull/19465))
|
||||
|
||||
@@ -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/).
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import tsConfig from '@appium/eslint-config-appium-ts';
|
||||
|
||||
export default [
|
||||
...tsConfig,
|
||||
{
|
||||
...tsConfig.find(({name}) => name === 'Test Files'),
|
||||
name: 'Test Support',
|
||||
files: [
|
||||
'packages/test-support/lib/**',
|
||||
'packages/driver-test-support/lib/**',
|
||||
'packages/plugin-test-support/lib/**',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Ignores',
|
||||
ignores: [
|
||||
'**/build-fixtures/**',
|
||||
'packages/appium/docs/**/assets/**',
|
||||
'packages/appium/docs/**/js/**',
|
||||
'packages/appium/sample-code/**',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
Generated
+8603
-4589
File diff suppressed because it is too large
Load Diff
+39
-17
@@ -64,7 +64,7 @@
|
||||
"sync-pkgs:appium-readme": "sync-monorepo-packages --force --no-package-json --packages=packages/appium README.md",
|
||||
"sync-pkgs:common-fields:default": "sync-monorepo-packages --packages=\"!packages/logger\" --fields=author,license,bugs,homepage,engines",
|
||||
"sync-pkgs:common-fields:logger": "sync-monorepo-packages --packages=\"packages/logger\" --fields=author,bugs,homepage,engines",
|
||||
"sync-pkgs:keywords": "sync-monorepo-packages --field=keywords --packages=\"packages/*\" --packages=\"!packages/eslint-config-appium\" --packages=\"!packages/types\" --packages=\"!packages/eslint-config-appium-ts\" ",
|
||||
"sync-pkgs:keywords": "sync-monorepo-packages --field=keywords --packages=\"packages/*\" --packages=\"!packages/types\" --packages=\"!packages/eslint-config-appium-ts\" ",
|
||||
"sync-pkgs:license": "sync-monorepo-packages --force --no-package-json --packages=\"!packages/logger\" LICENSE",
|
||||
"test": "run-s test:quick",
|
||||
"test:ci": "run-s test:smoke test:unit test:types test:e2e",
|
||||
@@ -85,44 +85,65 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@colors/colors": "1.6.0",
|
||||
"@eslint/js": "9.20.0",
|
||||
"@tsconfig/node14": "14.1.2",
|
||||
"@types/argparse": "2.0.17",
|
||||
"@types/archiver": "6.0.3",
|
||||
"@types/async-lock": "1.4.2",
|
||||
"@types/base64-stream": "1.0.5",
|
||||
"@types/bluebird": "3.5.42",
|
||||
"@types/chai": "5.0.1",
|
||||
"@types/chai-as-promised": "8.0.1",
|
||||
"@types/diff": "6.0.0",
|
||||
"@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.15",
|
||||
"@types/method-override": "3.0.0",
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/node": "22.10.1",
|
||||
"@types/mv": "2.1.4",
|
||||
"@types/ncp": "2.0.8",
|
||||
"@types/node": "22.13.4",
|
||||
"@types/pluralize": "0.0.33",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/serve-favicon": "2.5.7",
|
||||
"@types/shell-quote": "1.7.5",
|
||||
"@types/sinon": "17.0.3",
|
||||
"@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.5.14",
|
||||
"@types/xmldom": "0.1.34",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@typescript-eslint/eslint-plugin": "7.18.0",
|
||||
"@typescript-eslint/parser": "7.18.0",
|
||||
"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": "8.57.1",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-find-rules": "4.2.0",
|
||||
"eslint-import-resolver-typescript": "3.7.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "9.20.1",
|
||||
"eslint-config-prettier": "10.0.1",
|
||||
"eslint-import-resolver-typescript": "3.8.2",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-mocha": "10.5.0",
|
||||
"eslint-plugin-promise": "6.6.0",
|
||||
"eslint-plugin-promise": "7.2.1",
|
||||
"finalhandler": "1.3.1",
|
||||
"get-port": "5.1.1",
|
||||
"json-schema-to-typescript": "15.0.3",
|
||||
"json-schema-to-typescript": "15.0.4",
|
||||
"lerna": "8.1.9",
|
||||
"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.1",
|
||||
"rewiremock": "3.14.5",
|
||||
"rimraf": "5.0.10",
|
||||
"serve-static": "1.16.2",
|
||||
@@ -130,9 +151,10 @@
|
||||
"sync-monorepo-packages": "1.0.2",
|
||||
"ts-node": "10.9.2",
|
||||
"tsd": "0.31.2",
|
||||
"typescript": "5.7.2",
|
||||
"typescript": "5.7.3",
|
||||
"typescript-eslint": "8.24.1",
|
||||
"validate.js": "0.13.1",
|
||||
"webdriverio": "8.40.6",
|
||||
"webdriverio": "9.9.3",
|
||||
"ws": "8.18.0",
|
||||
"yaml-js": "0.3.1"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,84 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **appium,base-driver,base-plugin:** allow plugins to define custom bidi commands and emit bidi events ([#20876](https://github.com/appium/appium/issues/20876)) ([8df1c21](https://github.com/appium/appium/commit/8df1c217a15d30300c04b9f59cdbdffa70325828))
|
||||
|
||||
|
||||
|
||||
## [2.14.1](https://github.com/appium/appium/compare/appium@2.14.0...appium@2.14.1) (2025-01-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **appium:** Do not update BiDi commands for drivers which don't support it ([#20879](https://github.com/appium/appium/issues/20879)) ([13d8b03](https://github.com/appium/appium/commit/13d8b03cc185c5f4e0631002614981ee6f86fab4))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.5.1 ([#20875](https://github.com/appium/appium/issues/20875)) ([93e4288](https://github.com/appium/appium/commit/93e4288253c07a844e6e10f0dd040b1a40eb92ff))
|
||||
* **support:** update dependency teen_process to v2.2.3 ([#20882](https://github.com/appium/appium/issues/20882)) ([488e5de](https://github.com/appium/appium/commit/488e5dea82d3c525acf483952570e32f319f3722))
|
||||
|
||||
|
||||
|
||||
## [2.14.0](https://github.com/appium/appium/compare/appium@2.13.1...appium@2.14.0) (2025-01-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **appium:** Add session.status BiDi command ([#20839](https://github.com/appium/appium/issues/20839)) ([64e768e](https://github.com/appium/appium/commit/64e768efb7bebd6b5a24d55206d1cad00812777c))
|
||||
* **appium:** allow drivers to define their own bidi commands ([#20828](https://github.com/appium/appium/issues/20828)) ([a917ec6](https://github.com/appium/appium/commit/a917ec6ceda2166fb3dcbff6b2768f700db9e103))
|
||||
* **appium:** Update entry point resolution logic for ESM extensions ([#20866](https://github.com/appium/appium/issues/20866)) ([bbfc040](https://github.com/appium/appium/commit/bbfc04026f0629d89d8b313616b852896481d08d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **appium:** update CLI help messages ([#20825](https://github.com/appium/appium/issues/20825)) ([04e2d11](https://github.com/appium/appium/commit/04e2d1154b67c34607ab7a1e2a44b9b4bcfb998f))
|
||||
* **docutils:** update dependency yaml to v2.7.0 ([#20870](https://github.com/appium/appium/issues/20870)) ([389a3e8](https://github.com/appium/appium/commit/389a3e83ac0069504338f7182a4e938cd391de7e))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.4.5 ([#20847](https://github.com/appium/appium/issues/20847)) ([4474412](https://github.com/appium/appium/commit/4474412a60a82556f43fd3f3fb4f4ea2e4bc11ee))
|
||||
* **execute-driver-plugin:** update dependency webdriverio to v9.5.0 ([#20871](https://github.com/appium/appium/issues/20871)) ([990a066](https://github.com/appium/appium/commit/990a0667e3442bf01aaf67327066517d6310c371))
|
||||
* Reduce linter warnings ([#20860](https://github.com/appium/appium/issues/20860)) ([65658cc](https://github.com/appium/appium/commit/65658ccbdde9144c45cb5aad6a9089a5d6f3a0a3))
|
||||
* **types:** update dependency type-fest to v4 ([#20838](https://github.com/appium/appium/issues/20838)) ([a5897dd](https://github.com/appium/appium/commit/a5897dd25a277a42b0c650a52274ba2c891ac3b0))
|
||||
* **types:** update dependency type-fest to v4 ([#20843](https://github.com/appium/appium/issues/20843)) ([7abecad](https://github.com/appium/appium/commit/7abecaddd3ed64c7be321650b2a17990e74a7222))
|
||||
* **types:** update dependency type-fest to v4.31.0 ([#20857](https://github.com/appium/appium/issues/20857)) ([24abb38](https://github.com/appium/appium/commit/24abb385e54f57457c4fb3f2b654cb63645e7ccd))
|
||||
|
||||
|
||||
|
||||
## [2.13.1](https://github.com/appium/appium/compare/appium@2.13.0...appium@2.13.1) (2024-12-05)
|
||||
|
||||
|
||||
|
||||
@@ -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`**
|
||||
|
||||
@@ -320,9 +320,9 @@ your driver of the appropriate name, and it will be called when the BiDi command
|
||||
the client. To see which specific names you should use for BiDi commands, have a look at
|
||||
[bidi-commands.js](https://github.com/appium/appium/blob/master/packages/base-driver/lib/protocol/bidi-commands.js)
|
||||
|
||||
Currently, you also need to define a `doesSupportBidi` field on your driver instances, and ensure
|
||||
it is set to `true`. Appium will not turn on its Websocket servers for your driver and set up any
|
||||
handlers unless your driver says that it supports BiDi in this way.
|
||||
You are not limited to BiDi commands that are defined in the official BiDi specification. If you
|
||||
wish to define new commands, you may do so; you just need to tell Appium about them! See
|
||||
[below](#extend-the-existing-protocol-with-new-commands) for more information.
|
||||
|
||||
### Implement element finding
|
||||
|
||||
@@ -558,11 +558,9 @@ remote endpoint you're proxying to. Appium will take care of all the proxying fo
|
||||
### Proxy BiDi commands to another BiDi implementation
|
||||
|
||||
All of the above about proxying WebDriver commands is conceptually also valid for proxying BiDi
|
||||
commands specifically. In order to enable BiDi proxying, you need to:
|
||||
|
||||
1. Set the `doesSupportBidi` field on your driver instances to `true`.
|
||||
1. Implement `get bidiProxyUrl` on your driver. This should return a Websocket URL which is the
|
||||
address of the upstream socket you want BiDi commands to be proxied to.
|
||||
commands specifically. In order to enable BiDi proxying, you need to implement `get bidiProxyUrl`
|
||||
on your driver. This should return a Websocket URL which is the address of the upstream socket you
|
||||
want BiDi commands to be proxied to.
|
||||
|
||||
The intended pattern here is for you to start a session on the upstream implementation, check
|
||||
whether it has an active BiDi socket in the returned capabilities (e.g., the `webSocketUrl`
|
||||
@@ -574,9 +572,10 @@ to the upstream connection.
|
||||
|
||||
You may find that the existing commands don't cut it for your driver. If you want to expose
|
||||
behaviours that don't map to any of the existing commands, you can create new commands in one of
|
||||
two ways:
|
||||
three ways:
|
||||
|
||||
1. Extending the WebDriver protocol and creating client-side plugins to access the extensions
|
||||
1. Extending the classic WebDriver protocol and creating client-side plugins to access the extensions via the classic HTTP interface
|
||||
1. Extending the WebDriver BiDi protocol with new modules and methods, accessed from a client via the BiDi interface
|
||||
1. Overloading the Execute Script command by defining [Execute
|
||||
Methods](../guides/execute-methods.md)
|
||||
|
||||
@@ -606,7 +605,45 @@ won't have nice client-side functions designed to target these endpoints. So you
|
||||
create and release client-side plugins for each language you want to support (directions or
|
||||
examples can be found at the relevant client docs).
|
||||
|
||||
An alternative to this way of doing things is to overload a command which all WebDriver clients
|
||||
The second way of adding new commands is adding them as BiDi commands (accessed via the BiDi
|
||||
websocket interface, rather than the classic HTTP interface). BiDi commands come in two parts:
|
||||
a "module", which is basically a container or namespace, and a "command", which is the name of your
|
||||
new command.
|
||||
|
||||
As with the first method, you teach Appium to recognize your new BiDi commands by adding a static
|
||||
field to your driver class, called `newBidiCommands`. It has a format similar to `newMethodMap`.
|
||||
Basically it encapsulates information about the BiDi module, BiDi command name, reference to your
|
||||
driver instance method that will handle the command, and required and optional parameters. Here's
|
||||
an example of a `newBidiCommands` as implemented on an imaginary driver:
|
||||
|
||||
```js
|
||||
static newBidiCommands = {
|
||||
'appium:video': {
|
||||
startFramerateCapture: {
|
||||
command: 'startFrameCap',
|
||||
params: {
|
||||
required: ['videoSource'],
|
||||
optional: ['showOnScreen'],
|
||||
}
|
||||
},
|
||||
stopFramerateCapture: {
|
||||
command: 'stopFrameCap',
|
||||
},
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
In this imaginary example, we have defined two new BiDi commands:
|
||||
`appium:video.startFramerateCapture` and `appium:video.stopFramerateCapture`. Note first of all
|
||||
that, because we are defining a custom BiDi command, we should include a 'vendor prefix' (in this
|
||||
case, `appium:`, though you should pick something that represents your project). The first command
|
||||
takes a required and an optional parameter, and the second does not. When combined with generic
|
||||
BiDi support in your driver (see [the section on BiDi](#implement-webdriver-bidi-commands) above),
|
||||
and given an implementation of the appropriate methods on your driver (e.g. `startFrameCap` and
|
||||
`stopFrameCap` in this example), clients would be able to send these BiDi commands using whatever
|
||||
mechanism normally exists for doing so in the client library.
|
||||
|
||||
An alternative to these other ways of doing things is to overload a command which all WebDriver clients
|
||||
have access to already: Execute Script. Appium provides some a convenient tool for making this
|
||||
easy. Let's say you are building a driver for stereo system called `soundz`, and you wanted to
|
||||
create a command for playing a song by name. You could expose this to your users in such a way that
|
||||
@@ -657,6 +694,10 @@ A couple notes about this system:
|
||||
1. The `executeMethod` helper will reject with an error if a script name doesn't match one of the
|
||||
script names defined as a command in `executeMethodMap`, or if there are missing parameters.
|
||||
|
||||
One of the nice things about the Execute Method strategy is that methods implemented in this way
|
||||
will be available via the classic or BiDi interfaces (since they will result in the same Appium
|
||||
handlers being called).
|
||||
|
||||
|
||||
### Build Appium Doctor checks
|
||||
|
||||
@@ -711,7 +752,7 @@ in `this.currentCpuLoad`):
|
||||
|
||||
```js
|
||||
this.eventEmitter.emit('bidiEvent', {
|
||||
method: 'system.cpu',
|
||||
method: 'appium:system.cpu',
|
||||
params: {load: this.currentCpuLoad},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -325,9 +325,12 @@ If you want to offer functionality that doesn't map to any of the existing comma
|
||||
drivers, you can create new commands in one of two ways, just as is possible for drivers:
|
||||
|
||||
1. Extending the WebDriver protocol and creating client-side plugins to access the extensions
|
||||
1. Extending the WebDriver BiDi protocol with new modules and methods, accessed from a client via the BiDi interface
|
||||
1. Overloading the Execute Script command by defining [Execute
|
||||
Methods](../guides/execute-methods.md)
|
||||
|
||||
#### Extending the HTTP Protocol
|
||||
|
||||
If you want to follow the first path, you can direct Appium to recognize new methods and add them
|
||||
to its set of allowed HTTP routes and command names. You do this by assigning the `newMethodMap`
|
||||
static variable in your driver class to an object of the same form as Appium's `routes.js` object.
|
||||
@@ -368,7 +371,60 @@ clients won't have nice client-side functions designed to target these endpoints
|
||||
to create and release client-side plugins for each language you want to support (directions or
|
||||
examples can be found at the relevant client docs).
|
||||
|
||||
An alternative to this way of doing things is to overload a command which all WebDriver clients
|
||||
#### Extending the BiDi Protocol
|
||||
|
||||
You can also make new commands accessible via the WebDriver BiDi (WebSocket-based) protocol. BiDi
|
||||
commands come in two parts: a "module", which is basically a container or namespace, and
|
||||
a "command", which is the name of your new command.
|
||||
|
||||
As with the first method, you teach Appium to recognize your new BiDi commands by adding a static
|
||||
field to your driver class, called `newBidiCommands`. It has a format similar to `newMethodMap`.
|
||||
Basically it encapsulates information about the BiDi module, BiDi command name, reference to your
|
||||
driver instance method that will handle the command, and required and optional parameters. Here's
|
||||
an example of a `newBidiCommands` as implemented on an imaginary driver:
|
||||
|
||||
```js
|
||||
static newBidiCommands = {
|
||||
'appium:video': {
|
||||
startFramerateCapture: {
|
||||
command: 'startFrameCap',
|
||||
params: {
|
||||
required: ['videoSource'],
|
||||
optional: ['showOnScreen'],
|
||||
}
|
||||
},
|
||||
stopFramerateCapture: {
|
||||
command: 'stopFrameCap',
|
||||
},
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
In this imaginary example, we have defined two new BiDi commands: `appium:video.startFramerateCapture` and
|
||||
`appium:video.stopFramerateCapture`. The first command takes a required and an optional parameter, and the
|
||||
second does not. When you have implemented the `startFrameCap` and `stopFrameCap` methods on your
|
||||
plugin class, they will be called whenever the BiDi commands are triggered by a client. The
|
||||
signatures for these methods would look as follows:
|
||||
|
||||
```js
|
||||
async startFrameCap(next: () => Promise<any>, driver: DriverClass, videoSource: string, showOnScreen: boolean): Promise<any>;
|
||||
async stopFrameCap(next: () => Promise<any>, driver: DriverClass): Promise<any>;
|
||||
```
|
||||
|
||||
As with adding or overriding existing HTTP protocol commands, these methods are injected with
|
||||
a `next` parameter and a `driver` parameter. The `driver` object represents the driver currently
|
||||
owning the session, and calling `await next()` will execute/return the behavior that would execute
|
||||
were the plugin not active (i.e., the driver's own handling of that method, or the chain of
|
||||
commands executed with the same name by other active plugins).
|
||||
|
||||
Note that, currently, if a driver has BiDi proxying turned on, plugins will not be able to override
|
||||
BiDi methods handled by the proxy. Also note that since these are custom BiDi commands, our module
|
||||
name should include a vendor prefix (we chose `appium:`, but you can/should choose something that
|
||||
makes sense for your extension).
|
||||
|
||||
#### Overloading Execute Script
|
||||
|
||||
An alternative to these ways of doing things is to overload a command which all WebDriver clients
|
||||
have access to already: Execute Script. Make sure to read the section on [adding new
|
||||
commands](./build-drivers.md#extend-the-existing-protocol-with-new-commands) in
|
||||
the Building Drivers guide to understand the way this works in general. The way it works with
|
||||
@@ -405,6 +461,11 @@ Overriding Execute Methods from drivers works as you'd expect: if your plugin de
|
||||
Method with the same name as that of a driver, your command (in this case `plugMeIn`) will be
|
||||
called first. You can choose to run the driver's original behaviour via `next` if you want.
|
||||
|
||||
### Emit BiDi Events
|
||||
|
||||
Your plugin can emit custom BiDi events in [exactly the same way as Appium
|
||||
drivers](./build-drivers.md#emit-bidi-events).
|
||||
|
||||
### Build Appium Doctor checks
|
||||
|
||||
Your users can run `appium plugin doctor <pluginName>` to run installation and health checks. Visit
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ Here is a list of all the globally-recognized Appium capabilities:
|
||||
!!! info
|
||||
|
||||
Individual drivers and plugins can support other capabilities, so refer to their documentation
|
||||
for lists ofspecific capability names. Some drivers may also not support all of these capabilities
|
||||
for lists of specific capability names. Some drivers may also not support all of these capabilities
|
||||
|
||||
| <div style="width:12em">Capability</div> | Type | Required? | Description |
|
||||
|--------------------------------------------|-----------|-----------|----------------------------|
|
||||
@@ -67,6 +67,16 @@ Each Appium client has its own way of constructing capabilities and starting a s
|
||||
examples of doing this in each client library, head to the [Ecosystem](../ecosystem/index.md) page
|
||||
and click through to the appropriate client documentation.
|
||||
|
||||
## BiDi Protocol Support
|
||||
|
||||
Appium supports [WebDriver BiDi](https://w3c.github.io/webdriver-bidi/) protocol since base–driver 9.5.0.
|
||||
The actual behavior depends on individual drivers while the Appium and the baseーdriver support the protocol.
|
||||
Please make sure if a driver supports the protocol and what kind of commands/events it supports in the documentation.
|
||||
|
||||
| Capability Name | Type | Description |
|
||||
|---|---|−--|
|
||||
| `webSocketUrl` | `boolean` | To enable BiDi protocol in the session. |
|
||||
|
||||
## Using `appium:options` to Group Capabilities
|
||||
|
||||
If you use a lot of `appium:` capabilities in your tests, it can get a little repetitive. You can
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
+65
-426
@@ -1,6 +1,4 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import _ from 'lodash';
|
||||
import B from 'bluebird';
|
||||
import {getBuildInfo, updateBuildInfo, APPIUM_VER} from './config';
|
||||
import {
|
||||
BaseDriver,
|
||||
@@ -10,29 +8,24 @@ import {
|
||||
CREATE_SESSION_COMMAND,
|
||||
DELETE_SESSION_COMMAND,
|
||||
GET_STATUS_COMMAND,
|
||||
MAX_LOG_BODY_LENGTH,
|
||||
LIST_DRIVER_COMMANDS_COMMAND,
|
||||
LIST_DRIVER_EXTENSIONS_COMMAND,
|
||||
promoteAppiumOptions,
|
||||
promoteAppiumOptionsForObject,
|
||||
generateDriverLogPrefix,
|
||||
} from '@appium/base-driver';
|
||||
import AsyncLock from 'async-lock';
|
||||
import {
|
||||
parseCapsForInnerDriver,
|
||||
pullSettings,
|
||||
makeNonW3cCapsError,
|
||||
isBroadcastIp,
|
||||
fetchInterfaces,
|
||||
V4_BROADCAST_IP,
|
||||
validateFeatures,
|
||||
} from './utils';
|
||||
import {util, node, logger} from '@appium/support';
|
||||
import {util} from '@appium/support';
|
||||
import {getDefaultsForExtension} from './schema';
|
||||
import {DRIVER_TYPE, BIDI_BASE_PATH, BIDI_EVENT_NAME} from './constants';
|
||||
import WebSocket from 'ws';
|
||||
import os from 'node:os';
|
||||
|
||||
const MIN_WS_CODE_VAL = 1000;
|
||||
const MAX_WS_CODE_VAL = 1015;
|
||||
const WS_FALLBACK_CODE = 1011; // server encountered an error while fulfilling request
|
||||
import {DRIVER_TYPE, BIDI_BASE_PATH} from './constants';
|
||||
import * as bidiCommands from './bidi-commands';
|
||||
import * as inspectorCommands from './inspector-commands';
|
||||
|
||||
const desiredCapabilityConstraints = /** @type {const} */ ({
|
||||
automationName: {
|
||||
@@ -151,17 +144,6 @@ class AppiumDriver extends DriverCore {
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves logger instance for the current umbrella driver instance
|
||||
*/
|
||||
get log() {
|
||||
if (!this._log) {
|
||||
const instanceName = `${this.constructor.name}@${node.getObjectId(this).substring(0, 4)}`;
|
||||
this._log = logger.getLogger(instanceName);
|
||||
}
|
||||
return this._log;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel commands queueing for the umbrella Appium driver
|
||||
*/
|
||||
@@ -178,7 +160,6 @@ class AppiumDriver extends DriverCore {
|
||||
return this.sessions[sessionId];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-await
|
||||
async getStatus() {
|
||||
// https://www.w3.org/TR/webdriver/#dfn-status
|
||||
const statusObj = this._isShuttingDown
|
||||
@@ -214,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
|
||||
@@ -265,347 +251,6 @@ class AppiumDriver extends DriverCore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new bidi connection and set up handlers
|
||||
* @param {import('ws').WebSocket} ws The websocket connection object
|
||||
* @param {import('http').IncomingMessage} req The connection pathname, which might include the session id
|
||||
*/
|
||||
onBidiConnection(ws, req) {
|
||||
// TODO put bidi-related functionality into a mixin/helper class
|
||||
// wrap all of the handler logic with exception handling so if something blows up we can log
|
||||
// and close the websocket
|
||||
try {
|
||||
const {bidiHandlerDriver, proxyClient, send, sendToProxy, logSocketErr} = this.initBidiSocket(
|
||||
ws,
|
||||
req,
|
||||
);
|
||||
|
||||
this.initBidiSocketHandlers(
|
||||
ws,
|
||||
proxyClient,
|
||||
send,
|
||||
sendToProxy,
|
||||
bidiHandlerDriver,
|
||||
logSocketErr,
|
||||
);
|
||||
this.initBidiProxyHandlers(proxyClient, ws, send);
|
||||
this.initBidiEventListeners(ws, bidiHandlerDriver, send);
|
||||
} catch (err) {
|
||||
this.log.error(err);
|
||||
try {
|
||||
ws.close();
|
||||
} catch (ign) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new bidi connection
|
||||
* @param {import('ws').WebSocket} ws The websocket connection object
|
||||
* @param {import('http').IncomingMessage} req The connection pathname, which might include the session id
|
||||
*/
|
||||
initBidiSocket(ws, req) {
|
||||
let outOfBandErrorPrefix = '';
|
||||
const pathname = req.url;
|
||||
if (!pathname) {
|
||||
throw new Error('Invalid connection request: pathname missing from request');
|
||||
}
|
||||
const bidiSessionRe = new RegExp(`${BIDI_BASE_PATH}/([^/]+)$`);
|
||||
const bidiNoSessionRe = new RegExp(`${BIDI_BASE_PATH}/?$`);
|
||||
const sessionMatch = bidiSessionRe.exec(pathname);
|
||||
const noSessionMatch = bidiNoSessionRe.exec(pathname);
|
||||
|
||||
if (!sessionMatch && !noSessionMatch) {
|
||||
throw new Error(
|
||||
`Got websocket connection for path ${pathname} but didn't know what to do with it. ` +
|
||||
`Ignoring and will close the connection`,
|
||||
);
|
||||
}
|
||||
|
||||
// Let's figure out which driver is going to handle this socket connection. It's either going
|
||||
// to be a driver matching a session id appended to the bidi base path, or this umbrella driver
|
||||
// (if no session id is included in the bidi connection request)
|
||||
|
||||
/** @type {import('@appium/types').ExternalDriver | AppiumDriver} */
|
||||
let bidiHandlerDriver;
|
||||
|
||||
/** @type {import('ws').WebSocket | null} */
|
||||
let proxyClient = null;
|
||||
|
||||
if (sessionMatch) {
|
||||
// If we found a session id, see if it matches an active session
|
||||
const sessionId = sessionMatch[1];
|
||||
bidiHandlerDriver = this.sessions[sessionId];
|
||||
if (!bidiHandlerDriver) {
|
||||
// The session ID sent in doesn't match an active session; just ignore this socket
|
||||
// connection in that case
|
||||
throw new Error(
|
||||
`Got bidi connection request for session with id ${sessionId} which is closed ` +
|
||||
`or does not exist. Closing the socket connection.`,
|
||||
);
|
||||
}
|
||||
const driverName = bidiHandlerDriver.constructor.name;
|
||||
outOfBandErrorPrefix = `[session ${sessionId}] `;
|
||||
this.log.info(`Bidi websocket connection made for session ${sessionId}`);
|
||||
// store this socket connection for later removal on session deletion. theoretically there
|
||||
// can be multiple sockets per session
|
||||
if (!this.bidiSockets[sessionId]) {
|
||||
this.bidiSockets[sessionId] = [];
|
||||
}
|
||||
this.bidiSockets[sessionId].push(ws);
|
||||
|
||||
const bidiProxyUrl = bidiHandlerDriver.bidiProxyUrl;
|
||||
if (bidiProxyUrl) {
|
||||
try {
|
||||
new URL(bidiProxyUrl);
|
||||
} catch (ign) {
|
||||
throw new Error(
|
||||
`Got request for ${driverName} to proxy bidi connections to upstream socket with ` +
|
||||
`url ${bidiProxyUrl}, but this was not a valid url`,
|
||||
);
|
||||
}
|
||||
this.log.info(`Bidi connection for ${driverName} will be proxied to ${bidiProxyUrl}`);
|
||||
proxyClient = new WebSocket(bidiProxyUrl);
|
||||
this.bidiProxyClients[sessionId] = proxyClient;
|
||||
}
|
||||
} else {
|
||||
this.log.info('Bidi websocket connection made to main server');
|
||||
// no need to store the socket connection if it's to the main server since it will just
|
||||
// stay open as long as the server itself is and will close when the server closes.
|
||||
bidiHandlerDriver = this; // eslint-disable-line @typescript-eslint/no-this-alias
|
||||
}
|
||||
|
||||
const logSocketErr = (/** @type {Error} */ err) =>
|
||||
this.log.error(`${outOfBandErrorPrefix}${err}`);
|
||||
|
||||
// This is a function which wraps the 'send' method on a web socket for two reasons:
|
||||
// 1. Make it async-await friendly
|
||||
// 2. Do some logging if there's a send error
|
||||
const sendFactory = (/** @type {import('ws').WebSocket} */ socket) => {
|
||||
const socketSend = B.promisify(socket.send, {context: socket});
|
||||
return async (/** @type {string|Buffer} */ data) => {
|
||||
try {
|
||||
await socketSend(data);
|
||||
} catch (err) {
|
||||
logSocketErr(err);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Construct our send method for sending messages to the client
|
||||
const send = sendFactory(ws);
|
||||
|
||||
// Construct a conditional send method for proxying messages from the client to an upstream
|
||||
// bidi socket server (e.g. on a browser)
|
||||
const sendToProxy = proxyClient ? sendFactory(proxyClient) : null;
|
||||
|
||||
return {bidiHandlerDriver, proxyClient, send, sendToProxy, logSocketErr};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up handlers on upstream bidi socket we are proxying to/from
|
||||
*
|
||||
* @param {import('ws').WebSocket | null} proxyClient - the websocket connection to/from the
|
||||
* upstream socket (the one we're proxying to/from)
|
||||
* @param {import('ws').WebSocket} ws - the websocket connection to/from the client
|
||||
* @param {(data: string | Buffer) => Promise<void>} send - a method used to send data to the
|
||||
* client
|
||||
*/
|
||||
initBidiProxyHandlers(proxyClient, ws, send) {
|
||||
// Set up handlers for events that might come from the upstream bidi socket connection if
|
||||
// we're in proxy mode
|
||||
if (proxyClient) {
|
||||
// Here we're receiving a message from the upstream socket server. We want to pass it on to
|
||||
// the client
|
||||
proxyClient.on('message', async (/** @type {Buffer|string} */ data) => {
|
||||
const logData = _.truncate(data.toString('utf8'), {length: MAX_LOG_BODY_LENGTH});
|
||||
this.log.debug(
|
||||
`<-- BIDI Received data from proxied bidi socket, sending to client. Data: ${logData}`,
|
||||
);
|
||||
await send(data);
|
||||
});
|
||||
|
||||
// If the upstream socket server closes the connection, should close the connection to the
|
||||
// client as well
|
||||
proxyClient.on('close', (code, reason) => {
|
||||
this.log.debug(
|
||||
`Upstream bidi socket closed connection (code ${code}, reason: '${reason}'). ` +
|
||||
`Closing proxy connection to client`,
|
||||
);
|
||||
if (!_.isNumber(code)) {
|
||||
code = parseInt(code, 10);
|
||||
}
|
||||
if (_.isNaN(code) || code < MIN_WS_CODE_VAL || code > MAX_WS_CODE_VAL) {
|
||||
this.log.warn(
|
||||
`Received code ${code} from upstream socket, but this is not a valid ` +
|
||||
`websocket code. Rewriting to ${WS_FALLBACK_CODE} for ws compatibility`,
|
||||
);
|
||||
code = WS_FALLBACK_CODE;
|
||||
}
|
||||
ws.close(code, reason);
|
||||
});
|
||||
|
||||
proxyClient.on('error', (err) => {
|
||||
this.log.error(`Got error on upstream bidi socket connection: ${err}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up handlers on the bidi socket connection to the client
|
||||
*
|
||||
* @param {import('ws').WebSocket} ws - the websocket connection to/from the client
|
||||
* @param {import('ws').WebSocket | null} proxyClient - the websocket connection to/from the
|
||||
* upstream socket (the one we're proxying to/from, if we're proxying)
|
||||
* @param {(data: string | Buffer) => Promise<void>} send - a method used to send data to the
|
||||
* client
|
||||
* @param {((data: string | Buffer) => Promise<void>) | null} sendToProxy - a method used to send data to the
|
||||
* upstream socket
|
||||
* @param {import('@appium/types').ExternalDriver | AppiumDriver} bidiHandlerDriver - the driver
|
||||
* handling the bidi commands
|
||||
* @param {(err: Error) => void} logSocketErr - a special prefixed logger
|
||||
*/
|
||||
initBidiSocketHandlers(ws, proxyClient, send, sendToProxy, bidiHandlerDriver, logSocketErr) {
|
||||
ws.on('error', (err) => {
|
||||
// Can't do much with random errors on the connection other than log them
|
||||
logSocketErr(err);
|
||||
});
|
||||
|
||||
ws.on('open', () => {
|
||||
this.log.info('Bidi websocket connection is now open');
|
||||
});
|
||||
|
||||
// Now set up handlers for the various events that might happen on the websocket connection
|
||||
// coming from the client
|
||||
// First is incoming messages from the client
|
||||
ws.on('message', async (/** @type {Buffer} */ data) => {
|
||||
if (proxyClient) {
|
||||
const logData = _.truncate(data.toString('utf8'), {length: MAX_LOG_BODY_LENGTH});
|
||||
this.log.debug(
|
||||
`--> BIDI Received data from client, sending to upstream bidi socket. Data: ${logData}`,
|
||||
);
|
||||
// if we're meant to proxy to an upstream bidi socket, just do that
|
||||
// @ts-ignore sendToProxy is never null if proxyClient is truthy, but ts doesn't know
|
||||
// that
|
||||
await sendToProxy(data.toString('utf8'));
|
||||
} else {
|
||||
const res = await this.onBidiMessage(data, bidiHandlerDriver);
|
||||
await send(JSON.stringify(res));
|
||||
}
|
||||
});
|
||||
|
||||
// Next consider if the client closes the socket connection on us
|
||||
ws.on('close', (code, reason) => {
|
||||
// Not sure if we need to do anything here if the client closes the websocket connection.
|
||||
// Probably if a session was started via the socket, and the socket closes, we should end the
|
||||
// associated session to free up resources. But otherwise, for sockets attached to existing
|
||||
// sessions, doing nothing is probably right.
|
||||
this.log.debug(`Bidi socket connection closed (code ${code}, reason: '${reason}')`);
|
||||
|
||||
// If we're proxying, might as well close the upstream connection and clean it up
|
||||
if (proxyClient) {
|
||||
this.log.debug('Also closing bidi proxy socket connection');
|
||||
proxyClient.close(code, reason);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up bidi event listeners
|
||||
*
|
||||
* @param {import('ws').WebSocket} ws - the websocket connection to/from the client
|
||||
* @param {import('@appium/types').ExternalDriver | AppiumDriver} bidiHandlerDriver - the driver
|
||||
* handling the bidi commands
|
||||
* @param {(data: string | Buffer) => Promise<void>} send - a method used to send data to the
|
||||
* client
|
||||
*/
|
||||
initBidiEventListeners(ws, bidiHandlerDriver, send) {
|
||||
// If the driver emits a bidi event that should maybe get sent to the client, check to make
|
||||
// sure the client is subscribed and then pass it on
|
||||
let eventListener = async ({context, method, params}) => {
|
||||
// if the driver didn't specify a context, use the empty context
|
||||
if (!context) {
|
||||
context = '';
|
||||
}
|
||||
if (!method || !params) {
|
||||
throw new Error(
|
||||
`Driver emitted a bidi event that was malformed. Require method and params keys ` +
|
||||
`(with optional context). But instead received: ${JSON.stringify({
|
||||
context,
|
||||
method,
|
||||
params,
|
||||
})}`,
|
||||
);
|
||||
}
|
||||
if (ws.readyState !== WebSocket.OPEN) {
|
||||
// if the websocket is not still 'open', then we can ignore sending these events
|
||||
if (ws.readyState > WebSocket.OPEN) {
|
||||
// if the websocket is closed or closing, we can remove this listener as well to avoid
|
||||
// leaks
|
||||
bidiHandlerDriver.eventEmitter.removeListener(BIDI_EVENT_NAME, eventListener);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (bidiHandlerDriver.bidiEventSubs[method]?.includes(context)) {
|
||||
this.log.info(
|
||||
`<-- BIDI EVENT ${method} (context: '${context}', params: ${JSON.stringify(params)})`,
|
||||
);
|
||||
// now we can send the event onto the socket
|
||||
const ev = {type: 'event', context, method, params};
|
||||
await send(JSON.stringify(ev));
|
||||
}
|
||||
};
|
||||
bidiHandlerDriver.eventEmitter.on(BIDI_EVENT_NAME, eventListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer} data
|
||||
* @param {ExternalDriver | AppiumDriver} driver
|
||||
*/
|
||||
async onBidiMessage(data, driver) {
|
||||
let resMessage, id, method, params;
|
||||
const dataTruncated = _.truncate(data.toString(), {length: 100});
|
||||
try {
|
||||
try {
|
||||
({id, method, params} = JSON.parse(data.toString('utf8')));
|
||||
} catch (err) {
|
||||
throw new errors.InvalidArgumentError(
|
||||
`Could not parse Bidi command '${dataTruncated}': ${err.message}`,
|
||||
);
|
||||
}
|
||||
driver.log.info(`--> BIDI message #${id}`);
|
||||
if (!method) {
|
||||
throw new errors.InvalidArgumentError(
|
||||
`Missing method for BiDi operation in '${dataTruncated}'`,
|
||||
);
|
||||
}
|
||||
if (!params) {
|
||||
throw new errors.InvalidArgumentError(
|
||||
`Missing params for BiDi operation in '${dataTruncated}`,
|
||||
);
|
||||
}
|
||||
const result = await driver.executeBidiCommand(method, params);
|
||||
// https://w3c.github.io/webdriver-bidi/#protocol-definition
|
||||
resMessage = {
|
||||
id,
|
||||
type: 'success',
|
||||
result,
|
||||
};
|
||||
} catch (err) {
|
||||
resMessage = err.bidiErrObject(id);
|
||||
}
|
||||
driver.log.info(`<-- BIDI message #${id}`);
|
||||
return resMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a bidi server error
|
||||
* @param {Error} err
|
||||
*/
|
||||
onBidiServerError(err) {
|
||||
this.log.error(`Error from bidi websocket server: ${err}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new session
|
||||
* @param {W3CAppiumDriverCaps} jsonwpCaps JSONWP formatted desired capabilities
|
||||
@@ -695,6 +340,13 @@ class AppiumDriver extends DriverCore {
|
||||
driverInstance.relaxedSecurityEnabled = true;
|
||||
}
|
||||
|
||||
// We also want to assign any new Bidi Commands that the driver has specified, including all
|
||||
// the standard bidi commands. But add a method existence guard since some old driver class
|
||||
// instances might not have this method
|
||||
if (_.isFunction(driverInstance.updateBidiCommands)) {
|
||||
driverInstance.updateBidiCommands(InnerDriver.newBidiCommands ?? {});
|
||||
}
|
||||
|
||||
if (!_.isEmpty(this.args.denyInsecure)) {
|
||||
this.log.info('Explicitly preventing use of insecure features:');
|
||||
this.args.denyInsecure.map((a) => this.log.info(` ${a}`));
|
||||
@@ -759,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)) {
|
||||
@@ -776,14 +428,13 @@ class AppiumDriver extends DriverCore {
|
||||
await driverInstance.updateSettings(jwpSettings);
|
||||
}
|
||||
|
||||
// if the user has asked for bidi support, and the driver says that it supports bidi,
|
||||
// send our bidi url back to the user. The inner driver will need to have already saved any
|
||||
// internal bidi urls it might want to proxy to, cause we are going to overwrite that
|
||||
// information here!
|
||||
if (dCaps.webSocketUrl && driverInstance.doesSupportBidi) {
|
||||
// if the user has asked for bidi support, send our bidi url back to the user. The inner
|
||||
// driver will need to have already saved any internal bidi urls it might want to proxy to,
|
||||
// cause we are going to overwrite that information here!
|
||||
if (dCaps.webSocketUrl) {
|
||||
const {address, port, basePath} = this.args;
|
||||
const scheme = `ws${this.server.isSecure() ? 's' : ''}`;
|
||||
const host = 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 ` +
|
||||
@@ -922,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) {
|
||||
@@ -975,12 +600,13 @@ class AppiumDriver extends DriverCore {
|
||||
* Get the appropriate plugins for a session (or sessionless plugins)
|
||||
*
|
||||
* @param {?string} sessionId - the sessionId (or null) to use to find plugins
|
||||
* @returns {Array} - array of plugin instances
|
||||
* @returns {Array<import('@appium/types').Plugin>} - array of plugin instances
|
||||
*/
|
||||
pluginsForSession(sessionId = null) {
|
||||
if (sessionId) {
|
||||
if (!this.sessionPlugins[sessionId]) {
|
||||
this.sessionPlugins[sessionId] = this.createPluginInstances();
|
||||
const driverId = generateDriverLogPrefix(this.sessions[sessionId]);
|
||||
this.sessionPlugins[sessionId] = this.createPluginInstances(driverId || null);
|
||||
}
|
||||
return this.sessionPlugins[sessionId];
|
||||
}
|
||||
@@ -1011,14 +637,19 @@ class AppiumDriver extends DriverCore {
|
||||
|
||||
/**
|
||||
* Creates instances of all of the enabled Plugin classes
|
||||
* @param {string|null} driverId - ID to use for linking a driver to a plugin in logs
|
||||
* @returns {Plugin[]}
|
||||
*/
|
||||
createPluginInstances() {
|
||||
createPluginInstances(driverId = null) {
|
||||
/** @type {Plugin[]} */
|
||||
const pluginInstances = [];
|
||||
for (const [PluginClass, name] of this.pluginClasses.entries()) {
|
||||
const cliArgs = this.getCliArgsForPlugin(name);
|
||||
const plugin = new PluginClass(name, cliArgs);
|
||||
const plugin = new PluginClass(name, cliArgs, driverId);
|
||||
if (_.isFunction(/** @type {Plugin & ExtensionCore} */(plugin).updateBidiCommands)) {
|
||||
// some old plugin classes don't have `updateBidiCommands`
|
||||
/** @type {Plugin & ExtensionCore} */(plugin).updateBidiCommands(PluginClass.newBidiCommands ?? {});
|
||||
}
|
||||
pluginInstances.push(plugin);
|
||||
}
|
||||
return pluginInstances;
|
||||
@@ -1102,7 +733,9 @@ class AppiumDriver extends DriverCore {
|
||||
// if we're running with plugins, make sure we log that the default behavior is actually
|
||||
// happening so we can tell when the plugin call chain is unwrapping to the default behavior
|
||||
// if that's what happens
|
||||
plugins.length && this.log.info(`Executing default handling behavior for command '${cmd}'`);
|
||||
if (plugins.length) {
|
||||
this.log.info(`Executing default handling behavior for command '${cmd}'`);
|
||||
}
|
||||
|
||||
// if we make it here, we know that the default behavior is handled
|
||||
cmdHandledBy.default = true;
|
||||
@@ -1177,6 +810,12 @@ class AppiumDriver extends DriverCore {
|
||||
`to session ID ${sessionId}`,
|
||||
);
|
||||
this.sessionPlugins[sessionId] = this.sessionlessPlugins;
|
||||
for (const p of /** @type {(Plugin & ExtensionCore)[]} */(this.sessionPlugins[sessionId])) {
|
||||
if (_.isFunction(p.updateLogPrefix)) {
|
||||
// some old plugin classes don't have `updateLogPrefix` yet
|
||||
p.updateLogPrefix(`${generateDriverLogPrefix(p)} <${generateDriverLogPrefix(this.sessions[sessionId])}>`);
|
||||
}
|
||||
}
|
||||
this.sessionlessPlugins = [];
|
||||
}
|
||||
|
||||
@@ -1184,8 +823,9 @@ class AppiumDriver extends DriverCore {
|
||||
}
|
||||
|
||||
wrapCommandWithPlugins({driver, cmd, args, next, cmdHandledBy, plugins}) {
|
||||
plugins.length &&
|
||||
if (plugins.length) {
|
||||
this.log.info(`Plugins which can handle cmd '${cmd}': ${plugins.map((p) => p.name)}`);
|
||||
}
|
||||
|
||||
// now we can go through each plugin and wrap `next` around its own handler, passing the *old*
|
||||
// next in so that it can call it if it wants to
|
||||
@@ -1276,31 +916,29 @@ class AppiumDriver extends DriverCore {
|
||||
const dstSession = this.sessions[sessionId];
|
||||
return dstSession && dstSession.canProxy(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
// help decide which commands should be proxied to sub-drivers and which
|
||||
// should be handled by this, our umbrella driver
|
||||
function isAppiumDriverCommand(cmd) {
|
||||
return !isSessionCommand(cmd) || cmd === DELETE_SESSION_COMMAND;
|
||||
onBidiConnection = bidiCommands.onBidiConnection;
|
||||
onBidiMessage = bidiCommands.onBidiMessage;
|
||||
onBidiServerError = bidiCommands.onBidiServerError;
|
||||
cleanupBidiSockets = bidiCommands.cleanupBidiSockets;
|
||||
|
||||
listCommands = inspectorCommands.listCommands;
|
||||
listExtensions = inspectorCommands.listExtensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clients cannot use broadcast addresses, like 0.0.0.0 or ::
|
||||
* to create connections. Thus we prefer a hostname if such
|
||||
* address is provided or the actual address of a non-local interface,
|
||||
* in case the host only has one such interface.
|
||||
*
|
||||
* @param {string} address
|
||||
* @returns {string}
|
||||
* 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 determineBiDiHost(address) {
|
||||
if (!isBroadcastIp(address)) {
|
||||
return address;
|
||||
}
|
||||
|
||||
const nonLocalInterfaces = fetchInterfaces(address === V4_BROADCAST_IP ? 4 : 6)
|
||||
.filter((iface) => !iface.internal);
|
||||
return nonLocalInterfaces.length === 1 ? nonLocalInterfaces[0].address : os.hostname();
|
||||
function isAppiumDriverCommand(cmd) {
|
||||
return !isSessionCommand(cmd)
|
||||
|| _.includes([
|
||||
DELETE_SESSION_COMMAND,
|
||||
LIST_DRIVER_COMMANDS_COMMAND,
|
||||
LIST_DRIVER_EXTENSIONS_COMMAND,
|
||||
], cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1338,6 +976,7 @@ export {AppiumDriver};
|
||||
* @typedef {import('@appium/types').ExternalDriver} ExternalDriver
|
||||
* @typedef {import('@appium/types').PluginClass} PluginClass
|
||||
* @typedef {import('@appium/types').Plugin} Plugin
|
||||
* @typedef {import('@appium/base-driver').ExtensionCore} ExtensionCore
|
||||
* @typedef {import('@appium/types').DriverClass<import('@appium/types').Driver>} DriverClass
|
||||
*/
|
||||
|
||||
|
||||
@@ -0,0 +1,543 @@
|
||||
import _ from 'lodash';
|
||||
import B from 'bluebird';
|
||||
import {
|
||||
errors,
|
||||
ExtensionCore,
|
||||
} from '@appium/base-driver';
|
||||
import {BIDI_BASE_PATH, BIDI_EVENT_NAME} from './constants';
|
||||
import WebSocket from 'ws';
|
||||
import os from 'node:os';
|
||||
import {
|
||||
isBroadcastIp,
|
||||
fetchInterfaces,
|
||||
V4_BROADCAST_IP,
|
||||
} from './utils';
|
||||
import type {IncomingMessage} from 'node:http';
|
||||
import type {AppiumDriver} from './appium';
|
||||
import type {
|
||||
ErrorBiDiCommandResponse,
|
||||
SuccessBiDiCommandResponse,
|
||||
ExternalDriver,
|
||||
StringRecord,
|
||||
Plugin,
|
||||
BiDiResultData
|
||||
} from '@appium/types';
|
||||
|
||||
type ExtensionPlugin = Plugin & ExtensionCore
|
||||
type AnyDriver = ExternalDriver | AppiumDriver;
|
||||
type SendData = (data: string | Buffer) => Promise<void>;
|
||||
type LogSocketError = (err: Error) => void;
|
||||
interface InitBiDiSocketResult {
|
||||
bidiHandlerDriver: AnyDriver;
|
||||
bidiHandlerPlugins: ExtensionPlugin[];
|
||||
proxyClient: WebSocket | null;
|
||||
send: SendData;
|
||||
sendToProxy: SendData | null;
|
||||
logSocketErr: LogSocketError;
|
||||
}
|
||||
|
||||
|
||||
const MIN_WS_CODE_VAL = 1000;
|
||||
const MAX_WS_CODE_VAL = 1015;
|
||||
const WS_FALLBACK_CODE = 1011; // server encountered an error while fulfilling request
|
||||
const BIDI_EVENTS_MAP: WeakMap<AnyDriver, Record<string, number>> = new WeakMap();
|
||||
const MAX_LOGGED_DATA_LENGTH = 300;
|
||||
|
||||
/**
|
||||
* Clients cannot use broadcast addresses, like 0.0.0.0 or ::
|
||||
* to create connections. Thus we prefer a hostname if such
|
||||
* address is provided or the actual address of a non-local interface,
|
||||
* in case the host only has one such interface.
|
||||
*
|
||||
* @param address
|
||||
*/
|
||||
export function determineBiDiHost(address: string): string {
|
||||
if (!isBroadcastIp(address)) {
|
||||
return address;
|
||||
}
|
||||
|
||||
const nonLocalInterfaces = fetchInterfaces(address === V4_BROADCAST_IP ? 4 : 6)
|
||||
.filter((iface) => !iface.internal);
|
||||
return nonLocalInterfaces.length === 1 ? nonLocalInterfaces[0].address : os.hostname();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new bidi connection and set up handlers
|
||||
* @param ws The websocket connection object
|
||||
* @param req The connection pathname, which might include the session id
|
||||
*/
|
||||
export function onBidiConnection(this: AppiumDriver, ws: WebSocket, req: IncomingMessage): void {
|
||||
try {
|
||||
const initBiDiSocketFunc: OmitThisParameter<typeof initBidiSocket> = initBidiSocket.bind(this);
|
||||
const {bidiHandlerDriver, bidiHandlerPlugins, proxyClient, send, sendToProxy, logSocketErr} = initBiDiSocketFunc(
|
||||
ws,
|
||||
req,
|
||||
);
|
||||
|
||||
const initBidiSocketHandlersFunc: OmitThisParameter<typeof initBidiSocketHandlers> = initBidiSocketHandlers
|
||||
.bind(this);
|
||||
initBidiSocketHandlersFunc(
|
||||
ws,
|
||||
proxyClient,
|
||||
send,
|
||||
sendToProxy,
|
||||
bidiHandlerDriver,
|
||||
bidiHandlerPlugins,
|
||||
logSocketErr,
|
||||
);
|
||||
if (proxyClient) {
|
||||
const initBidiProxyHandlersFunc: OmitThisParameter<typeof initBidiProxyHandlers> = initBidiProxyHandlers
|
||||
.bind(bidiHandlerDriver);
|
||||
initBidiProxyHandlersFunc(proxyClient, ws, send);
|
||||
}
|
||||
const initBidiEventListenersFunc: OmitThisParameter<typeof initBidiEventListeners> = initBidiEventListeners
|
||||
.bind(this);
|
||||
initBidiEventListenersFunc(ws, bidiHandlerDriver, bidiHandlerPlugins, send);
|
||||
} catch (err) {
|
||||
this.log.error(err);
|
||||
try {
|
||||
ws.close();
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function wrapCommandWithPlugins(driver: ExtensionCore, plugins: ExtensionCore[], method: string, params: StringRecord): () => Promise<BiDiResultData> {
|
||||
const [moduleName, methodName] = method.split('.');
|
||||
let next = async () => await driver.executeBidiCommand(method, params);
|
||||
for (const plugin of plugins.filter((p) => p.doesBidiCommandExist(moduleName, methodName))) {
|
||||
next = ((_next) => async () => await plugin.executeBidiCommand(method, params, _next, driver))(next);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data
|
||||
* @param driver
|
||||
* @param plugins
|
||||
*/
|
||||
export async function onBidiMessage(
|
||||
this: AppiumDriver,
|
||||
data: Buffer,
|
||||
driver: AnyDriver,
|
||||
plugins: ExtensionPlugin[]
|
||||
): Promise<SuccessBiDiCommandResponse | ErrorBiDiCommandResponse> {
|
||||
let resMessage: SuccessBiDiCommandResponse | ErrorBiDiCommandResponse;
|
||||
let id: number = 0;
|
||||
const driverLog = driver.log;
|
||||
const dataTruncated = _.truncate(data.toString(), {length: MAX_LOGGED_DATA_LENGTH});
|
||||
try {
|
||||
let method: string;
|
||||
let params: StringRecord;
|
||||
try {
|
||||
({id, method, params} = JSON.parse(data.toString('utf8')));
|
||||
} catch (err) {
|
||||
throw new errors.InvalidArgumentError(
|
||||
`Could not parse Bidi command '${dataTruncated}': ${err.message}`,
|
||||
);
|
||||
}
|
||||
driverLog.info(`--> BIDI message #${id}`);
|
||||
if (!method) {
|
||||
throw new errors.InvalidArgumentError(
|
||||
`Missing method for BiDi operation in '${dataTruncated}'`,
|
||||
);
|
||||
}
|
||||
if (!params) {
|
||||
throw new errors.InvalidArgumentError(
|
||||
`Missing params for BiDi operation in '${dataTruncated}`,
|
||||
);
|
||||
}
|
||||
const executeWrappedCommand = wrapCommandWithPlugins(driver as ExtensionCore, plugins, method, params);
|
||||
const result = await executeWrappedCommand();
|
||||
resMessage = {
|
||||
id,
|
||||
type: 'success',
|
||||
result,
|
||||
};
|
||||
} catch (err) {
|
||||
resMessage = _.has(err, 'bidiErrObject')
|
||||
? err.bidiErrObject(id)
|
||||
: {
|
||||
id,
|
||||
type: 'error',
|
||||
error: errors.UnknownError.error(),
|
||||
message: (err as Error).message,
|
||||
stacktrace: (err as Error).stack,
|
||||
};
|
||||
}
|
||||
driverLog.info(`<-- BIDI message #${id}`);
|
||||
return resMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a bidi server error
|
||||
* @param err
|
||||
*/
|
||||
export function onBidiServerError(this: AppiumDriver, err: Error): void {
|
||||
this.log.warn(`Error from bidi websocket server: ${err}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param req The connection pathname, which might include the session id
|
||||
*/
|
||||
function initBidiSocket(this: AppiumDriver, ws: WebSocket, req: IncomingMessage): InitBiDiSocketResult {
|
||||
const pathname = req.url;
|
||||
if (!pathname) {
|
||||
throw new Error('Invalid connection request: pathname missing from request');
|
||||
}
|
||||
const bidiSessionRe = new RegExp(`${BIDI_BASE_PATH}/([^/]+)$`);
|
||||
const bidiNoSessionRe = new RegExp(`${BIDI_BASE_PATH}/?$`);
|
||||
const sessionMatch = bidiSessionRe.exec(pathname);
|
||||
const noSessionMatch = bidiNoSessionRe.exec(pathname);
|
||||
|
||||
if (!sessionMatch && !noSessionMatch) {
|
||||
throw new Error(
|
||||
`Got websocket connection for path ${pathname} but didn't know what to do with it. ` +
|
||||
`Ignoring and will close the connection`,
|
||||
);
|
||||
}
|
||||
|
||||
// Let's figure out which driver is going to handle this socket connection. It's either going
|
||||
// to be a driver matching a session id appended to the bidi base path, or this umbrella driver
|
||||
// (if no session id is included in the bidi connection request)
|
||||
|
||||
let bidiHandlerDriver: AnyDriver;
|
||||
let proxyClient: WebSocket | null = null;
|
||||
const bidiHandlerPlugins: ExtensionPlugin[] = [];
|
||||
if (sessionMatch) {
|
||||
// If we found a session id, see if it matches an active session
|
||||
const sessionId = sessionMatch[1];
|
||||
bidiHandlerDriver = this.sessions[sessionId];
|
||||
if (!bidiHandlerDriver) {
|
||||
// The session ID sent in doesn't match an active session; just ignore this socket
|
||||
// connection in that case
|
||||
throw new Error(
|
||||
`Got bidi connection request for session with id ${sessionId} which is closed ` +
|
||||
`or does not exist. Closing the socket connection.`,
|
||||
);
|
||||
}
|
||||
const driverName = bidiHandlerDriver.constructor.name;
|
||||
this.log.info(`Bidi websocket connection made for session ${sessionId}`);
|
||||
// store this socket connection for later removal on session deletion. theoretically there
|
||||
// can be multiple sockets per session
|
||||
if (!this.bidiSockets[sessionId]) {
|
||||
this.bidiSockets[sessionId] = [];
|
||||
}
|
||||
this.bidiSockets[sessionId].push(ws);
|
||||
|
||||
const bidiProxyUrl = bidiHandlerDriver.bidiProxyUrl;
|
||||
if (bidiProxyUrl) {
|
||||
try {
|
||||
new URL(bidiProxyUrl);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Got request for ${driverName} to proxy bidi connections to upstream socket with ` +
|
||||
`url ${bidiProxyUrl}, but this was not a valid url`,
|
||||
);
|
||||
}
|
||||
this.log.info(`Bidi connection for ${driverName} will be proxied to ${bidiProxyUrl}. ` +
|
||||
`Plugins will not handle bidi commands`);
|
||||
proxyClient = new WebSocket(bidiProxyUrl);
|
||||
this.bidiProxyClients[sessionId] = proxyClient;
|
||||
} else {
|
||||
bidiHandlerPlugins.push(...this.pluginsForSession(sessionId) as ExtensionPlugin[]);
|
||||
}
|
||||
} else {
|
||||
this.log.info('Bidi websocket connection made to main server');
|
||||
// no need to store the socket connection if it's to the main server since it will just
|
||||
// stay open as long as the server itself is and will close when the server closes.
|
||||
bidiHandlerDriver = this; // eslint-disable-line @typescript-eslint/no-this-alias
|
||||
bidiHandlerPlugins.push(...this.pluginsForSession() as ExtensionPlugin[]);
|
||||
}
|
||||
|
||||
const driverLog = bidiHandlerDriver.log;
|
||||
const logSocketErr: LogSocketError = (err: Error) => {
|
||||
driverLog.warn(err.message);
|
||||
};
|
||||
|
||||
// This is a function which wraps the 'send' method on a web socket for two reasons:
|
||||
// 1. Make it async-await friendly
|
||||
// 2. Do some logging if there's a send error
|
||||
const sendFactory = (socket: WebSocket) => {
|
||||
const socketSend = B.promisify(socket.send, {context: socket});
|
||||
return async (data: string | Buffer) => {
|
||||
try {
|
||||
await assertIsOpen(socket);
|
||||
await socketSend(data);
|
||||
} catch (err) {
|
||||
logSocketErr(err);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Construct our send method for sending messages to the client
|
||||
const send: SendData = sendFactory(ws);
|
||||
|
||||
// Construct a conditional send method for proxying messages from the client to an upstream
|
||||
// bidi socket server (e.g. on a browser)
|
||||
const sendToProxy: SendData | null = proxyClient ? sendFactory(proxyClient) : null;
|
||||
|
||||
return {bidiHandlerDriver, bidiHandlerPlugins, proxyClient, send, sendToProxy, logSocketErr};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up handlers on upstream bidi socket we are proxying to/from
|
||||
*
|
||||
* @param proxyClient - the websocket connection to/from the
|
||||
* upstream socket (the one we're proxying to/from)
|
||||
* @param ws - the websocket connection to/from the client
|
||||
* @param send - a method used to send data to the
|
||||
* client
|
||||
*/
|
||||
function initBidiProxyHandlers(
|
||||
this: AnyDriver,
|
||||
proxyClient: WebSocket,
|
||||
ws: WebSocket,
|
||||
send: SendData,
|
||||
): void {
|
||||
// Set up handlers for events that might come from the upstream bidi socket connection if
|
||||
// we're in proxy mode
|
||||
const driverLog = this.log;
|
||||
|
||||
// Here we're receiving a message from the upstream socket server. We want to pass it on to
|
||||
// the client
|
||||
proxyClient.on('message', send);
|
||||
|
||||
// If the upstream socket server closes the connection, should close the connection to the
|
||||
// client as well
|
||||
proxyClient.on('close', (code, reason) => {
|
||||
driverLog.debug(
|
||||
`Upstream bidi socket closed connection (code ${code}, reason: '${reason}'). ` +
|
||||
`Closing proxy connection to client`,
|
||||
);
|
||||
const intCode: number = _.isNumber(code) ? (code as number) : parseInt(code, 10);
|
||||
if (_.isNaN(intCode) || intCode < MIN_WS_CODE_VAL || intCode > MAX_WS_CODE_VAL) {
|
||||
driverLog.warn(
|
||||
`Received code ${code} from upstream socket, but this is not a valid ` +
|
||||
`websocket code. Rewriting to ${WS_FALLBACK_CODE} for ws compatibility`,
|
||||
);
|
||||
code = WS_FALLBACK_CODE;
|
||||
}
|
||||
ws.close(code, reason);
|
||||
});
|
||||
|
||||
proxyClient.on('error', (err) => {
|
||||
driverLog.warn(`Got error on upstream bidi socket connection: ${err.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up handlers on the bidi socket connection to the client
|
||||
*
|
||||
* @param ws - the websocket connection to/from the client
|
||||
* @param proxyClient - the websocket connection to/from the
|
||||
* upstream socket (the one we're proxying to/from, if we're proxying)
|
||||
* @param send - a method used to send data to the
|
||||
* client
|
||||
* @param sendToProxy - a method used to send data to the
|
||||
* upstream socket
|
||||
* @param bidiHandlerDriver - the driver
|
||||
* handling the bidi commands
|
||||
* @param bidiHandlerPlugins - plugins that might also handle bidi commands
|
||||
* @param logSocketErr - a special prefixed logger
|
||||
*/
|
||||
function initBidiSocketHandlers(
|
||||
this: AppiumDriver,
|
||||
ws: WebSocket,
|
||||
proxyClient: WebSocket | null,
|
||||
send: SendData,
|
||||
sendToProxy: SendData | null,
|
||||
bidiHandlerDriver: AnyDriver,
|
||||
bidiHandlerPlugins: ExtensionPlugin[],
|
||||
logSocketErr: LogSocketError,
|
||||
): void {
|
||||
const driverLog = bidiHandlerDriver.log;
|
||||
// Can't do much with random errors on the connection other than log them
|
||||
ws.on('error', logSocketErr);
|
||||
|
||||
ws.on('open', () => {
|
||||
driverLog.info('BiDi websocket connection is now open');
|
||||
});
|
||||
|
||||
// Now set up handlers for the various events that might happen on the websocket connection
|
||||
// coming from the client
|
||||
// First is incoming messages from the client
|
||||
ws.on('message', async (data: Buffer) => {
|
||||
if (proxyClient && sendToProxy) {
|
||||
// if we're meant to proxy to an upstream bidi socket, just do that
|
||||
// TODO trying to determine how this proxying behaviour would interface with plugins is too
|
||||
// complex for now, so just ignore plugins in this case
|
||||
await sendToProxy(data.toString('utf8'));
|
||||
} else {
|
||||
const res = await this.onBidiMessage(data, bidiHandlerDriver, bidiHandlerPlugins);
|
||||
await send(JSON.stringify(res));
|
||||
}
|
||||
});
|
||||
|
||||
// Next consider if the client closes the socket connection on us
|
||||
ws.on('close', (code, reason) => {
|
||||
// Not sure if we need to do anything here if the client closes the websocket connection.
|
||||
// Probably if a session was started via the socket, and the socket closes, we should end the
|
||||
// associated session to free up resources. But otherwise, for sockets attached to existing
|
||||
// sessions, doing nothing is probably right.
|
||||
driverLog.debug(`BiDi socket connection closed (code ${code}, reason: '${reason}')`);
|
||||
|
||||
// If we're proxying, might as well close the upstream connection and clean it up
|
||||
if (proxyClient) {
|
||||
driverLog.debug('Also closing BiDi proxy socket connection');
|
||||
proxyClient.close(code, reason);
|
||||
}
|
||||
|
||||
const eventLogCounts = BIDI_EVENTS_MAP.get(bidiHandlerDriver);
|
||||
if (!_.isEmpty(eventLogCounts)) {
|
||||
driverLog.debug(`BiDi events statistics: ${JSON.stringify(eventLogCounts, null, 2)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up bidi event listeners
|
||||
*
|
||||
* @param ws - the websocket connection to/from the client
|
||||
* @param bidiHandlerDriver - the driver
|
||||
* handling the bidi commands
|
||||
* @param send - a method used to send data to the
|
||||
* client
|
||||
*/
|
||||
function initBidiEventListeners(
|
||||
this: AppiumDriver,
|
||||
ws: WebSocket,
|
||||
bidiHandlerDriver: AnyDriver,
|
||||
bidiHandlerPlugins: ExtensionPlugin[],
|
||||
send: SendData,
|
||||
): void {
|
||||
// If the driver emits a bidi event that should maybe get sent to the client, check to make
|
||||
// sure the client is subscribed and then pass it on
|
||||
const eventLogCounts: Record<string, number> = BIDI_EVENTS_MAP.get(bidiHandlerDriver) ?? {};
|
||||
BIDI_EVENTS_MAP.set(bidiHandlerDriver, eventLogCounts);
|
||||
const eventListenerFactory = (extType: 'driver'|'plugin', ext: ExtensionCore) => {
|
||||
const eventListener = async ({context, method, params = {}}) => {
|
||||
// if the driver didn't specify a context, use the empty context
|
||||
if (!context) {
|
||||
context = '';
|
||||
}
|
||||
if (!method || !params) {
|
||||
ext.log?.warn( // some old plugins might not have the `log` property
|
||||
`${_.capitalize(extType)} emitted a bidi event that was malformed. Require method and params keys ` +
|
||||
`(with optional context). But instead received: ${_.truncate(JSON.stringify({
|
||||
context,
|
||||
method,
|
||||
params,
|
||||
}), {length: MAX_LOGGED_DATA_LENGTH})}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (ws.readyState !== WebSocket.OPEN) {
|
||||
// if the websocket is not still 'open', then we can ignore sending these events
|
||||
if (ws.readyState > WebSocket.OPEN) {
|
||||
// if the websocket is closed or closing, we can remove this listener as well to avoid
|
||||
// leaks. Some old plugin classes might not have the `eventEmitter` property, so use an
|
||||
// existence guard for now.
|
||||
ext.eventEmitter?.removeListener(BIDI_EVENT_NAME, eventListener);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const eventSubs = bidiHandlerDriver.bidiEventSubs[method];
|
||||
if (_.isArray(eventSubs) && eventSubs.includes(context)) {
|
||||
if (method in eventLogCounts) {
|
||||
++eventLogCounts[method];
|
||||
} else {
|
||||
ext.log?.info( // some old plugins might not have the `log` property
|
||||
`<-- BIDI EVENT ${method} (context: '${context}', ` +
|
||||
`params: ${_.truncate(JSON.stringify(params), {length: MAX_LOGGED_DATA_LENGTH})}). ` +
|
||||
`All further similar events won't be logged.`,
|
||||
);
|
||||
eventLogCounts[method] = 1;
|
||||
}
|
||||
// now we can send the event onto the socket
|
||||
const ev = {type: 'event', context, method, params};
|
||||
await send(JSON.stringify(ev));
|
||||
}
|
||||
};
|
||||
return eventListener;
|
||||
};
|
||||
bidiHandlerDriver.eventEmitter.on(BIDI_EVENT_NAME, eventListenerFactory('driver', bidiHandlerDriver as ExtensionCore));
|
||||
for (const plugin of bidiHandlerPlugins) {
|
||||
// some old plugins might not have the eventEmitter property
|
||||
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
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import {INSTALL_TYPES} from '../extension/extension-config';
|
||||
import {toParserArgs} from '../schema/cli-args';
|
||||
const DRIVER_EXAMPLE = 'xcuitest';
|
||||
const PLUGIN_EXAMPLE = 'find_by_image';
|
||||
const PLUGIN_EXAMPLE = 'images';
|
||||
|
||||
/**
|
||||
* This is necessary because we pass the array into `argparse`. `argparse` is bad and mutates things. We don't want that.
|
||||
@@ -32,7 +32,7 @@ const globalExtensionArgs = new Map([
|
||||
required: false,
|
||||
default: false,
|
||||
action: 'store_true',
|
||||
help: 'Use JSON for output format',
|
||||
help: 'Return output in JSON format',
|
||||
dest: 'json',
|
||||
},
|
||||
],
|
||||
@@ -82,7 +82,7 @@ function makeListArgs(type) {
|
||||
required: false,
|
||||
default: false,
|
||||
action: 'store_true',
|
||||
help: 'Show information about newer versions',
|
||||
help: `Show information about available ${type} updates`,
|
||||
dest: 'showUpdates',
|
||||
},
|
||||
],
|
||||
@@ -92,7 +92,7 @@ function makeListArgs(type) {
|
||||
required: false,
|
||||
default: false,
|
||||
action: 'store_true',
|
||||
help: 'Show more information about each extension',
|
||||
help: `Show more information about each ${type}`,
|
||||
dest: 'verbose',
|
||||
},
|
||||
],
|
||||
@@ -112,9 +112,8 @@ function makeInstallArgs(type) {
|
||||
{
|
||||
type: 'str',
|
||||
help:
|
||||
`Name of the ${type} to install, for example: ` + type === DRIVER_TYPE
|
||||
? DRIVER_EXAMPLE
|
||||
: PLUGIN_EXAMPLE,
|
||||
`Name of the ${type} to install, for example: ` +
|
||||
(type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -124,7 +123,7 @@ function makeInstallArgs(type) {
|
||||
default: null,
|
||||
choices: INSTALL_TYPES_ARRAY,
|
||||
help:
|
||||
`Where to look for the ${type} if it is not one of Appium's verified ` +
|
||||
`Where to look for the ${type} if it is not one of Appium's official ` +
|
||||
`${type}s. Possible values: ${INSTALL_TYPES_ARRAY.join(', ')}`,
|
||||
dest: 'installType',
|
||||
},
|
||||
@@ -136,9 +135,8 @@ function makeInstallArgs(type) {
|
||||
default: null,
|
||||
type: 'str',
|
||||
help:
|
||||
`If installing from Git or GitHub, the package name, as defined in the plugin's ` +
|
||||
`package.json file in the "name" field, cannot be determined automatically, and ` +
|
||||
`should be reported here, otherwise the install will probably fail.`,
|
||||
`The Node.js package name of the ${type}. ` +
|
||||
`Required if "source" is set to "git" or "github".`,
|
||||
dest: 'packageName',
|
||||
},
|
||||
],
|
||||
@@ -158,9 +156,8 @@ function makeUninstallArgs(type) {
|
||||
{
|
||||
type: 'str',
|
||||
help:
|
||||
`Name of the ${type} to uninstall, for example: ` + type === DRIVER_TYPE
|
||||
? DRIVER_EXAMPLE
|
||||
: PLUGIN_EXAMPLE,
|
||||
`Name of the ${type} to uninstall, for example: ` +
|
||||
(type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
|
||||
},
|
||||
],
|
||||
]);
|
||||
@@ -179,9 +176,8 @@ function makeDoctorArgs(type) {
|
||||
{
|
||||
type: 'str',
|
||||
help:
|
||||
`Name of the ${type} to run doctor checks for, for example: ` + type === DRIVER_TYPE
|
||||
? DRIVER_EXAMPLE
|
||||
: PLUGIN_EXAMPLE,
|
||||
`Name of the ${type} to run doctor checks for, for example: ` +
|
||||
(type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
|
||||
},
|
||||
],
|
||||
]);
|
||||
@@ -200,13 +196,10 @@ function makeUpdateArgs(type) {
|
||||
{
|
||||
type: 'str',
|
||||
help:
|
||||
`Name of the ${type} to update, or the word "installed" to update all installed ` +
|
||||
`${type}s. To see available updates, run "appium ${type} list --installed --updates". ` +
|
||||
'For example: ' +
|
||||
type ===
|
||||
DRIVER_TYPE
|
||||
? DRIVER_EXAMPLE
|
||||
: PLUGIN_EXAMPLE,
|
||||
`Name of the ${type} to update, or "installed" to update all installed ${type}s. ` +
|
||||
`To see available ${type} updates, run "appium ${type} list --installed --updates". ` +
|
||||
'For example: ' +
|
||||
(type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -216,8 +209,7 @@ function makeUpdateArgs(type) {
|
||||
default: false,
|
||||
action: 'store_true',
|
||||
help:
|
||||
`Include updates that might have a new major revision, and potentially include ` +
|
||||
`breaking changes`,
|
||||
'Include any available major revision updates, which may have breaking changes',
|
||||
},
|
||||
],
|
||||
]);
|
||||
@@ -236,9 +228,8 @@ function makeRunArgs(type) {
|
||||
{
|
||||
type: 'str',
|
||||
help:
|
||||
`Name of the ${type} to run a script from, for example: ` + type === DRIVER_TYPE
|
||||
? DRIVER_EXAMPLE
|
||||
: PLUGIN_EXAMPLE,
|
||||
`Name of the ${type} to run a script from, for example: ` +
|
||||
(type === DRIVER_TYPE ? DRIVER_EXAMPLE : PLUGIN_EXAMPLE),
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -248,9 +239,8 @@ function makeRunArgs(type) {
|
||||
nargs: '?',
|
||||
type: 'str',
|
||||
help:
|
||||
`Name of the script to run from the ${type}. The script name must be a key ` +
|
||||
`inside the "appium.scripts" field inside the ${type}'s "package.json" file. ` +
|
||||
`If not provided then all available script names are going to be listed.`,
|
||||
`Name of the ${type} script to run. If not provided, return a list ` +
|
||||
`of available scripts for this ${type}.`,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-console */
|
||||
import B from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
@@ -18,7 +17,7 @@ import {inspect} from 'node:util';
|
||||
import {pathToFileURL} from 'url';
|
||||
import {Doctor, EXIT_CODE as DOCTOR_EXIT_CODE} from '../doctor/doctor';
|
||||
import {npmPackage} from '../utils';
|
||||
import semver from 'semver';
|
||||
import * as semver from 'semver';
|
||||
|
||||
const UPDATE_ALL = 'installed';
|
||||
|
||||
@@ -486,10 +485,11 @@ class ExtensionCliCommand {
|
||||
getInstallationReceipt({pkg, installPath, installType, installSpec}) {
|
||||
const {appium, name, version, peerDependencies} = pkg;
|
||||
|
||||
const strVersion = /** @type {string} */ (version);
|
||||
/** @type {import('appium/types').InternalMetadata} */
|
||||
const internal = {
|
||||
pkgName: name,
|
||||
version,
|
||||
pkgName: /** @type {string} */ (name),
|
||||
version: strVersion,
|
||||
installType,
|
||||
installSpec,
|
||||
installPath,
|
||||
|
||||
@@ -18,9 +18,11 @@ import {finalizeSchema, getArgSpec, hasArgSpec} from '../schema';
|
||||
import {rootDir} from '../config';
|
||||
import {getExtensionArgs, getServerArgs} from './args';
|
||||
import {
|
||||
DEFAULT_PLUGINS,
|
||||
SUBCOMMAND_MOBILE,
|
||||
SUBCOMMAND_DESKTOP,
|
||||
SUBCOMMAND_BROWSER,
|
||||
SUBCOMMAND_RESET,
|
||||
getPresetDrivers,
|
||||
determinePlatformName
|
||||
} from './setup-command';
|
||||
@@ -53,7 +55,8 @@ class ArgParser {
|
||||
const parser = new ArgumentParser({
|
||||
add_help: true,
|
||||
description:
|
||||
'A webdriver-compatible server that facilitates automation of web, mobile, and other types of apps across various platforms.',
|
||||
'A webdriver-compatible server that facilitates automation of web, mobile, and other ' +
|
||||
'types of apps across various platforms.',
|
||||
prog,
|
||||
});
|
||||
|
||||
@@ -84,9 +87,6 @@ class ArgParser {
|
||||
|
||||
const subParsers = parser.add_subparsers({dest: 'subcommand'});
|
||||
|
||||
// add the 'setup' command
|
||||
ArgParser._addSetupToParser(subParsers);
|
||||
|
||||
// add the 'server' subcommand, and store the raw arguments on the parser
|
||||
// object as a way for other parts of the code to work with the arguments
|
||||
// conceptually rather than just through argparse
|
||||
@@ -97,6 +97,9 @@ class ArgParser {
|
||||
// add the 'driver' and 'plugin' subcommands
|
||||
ArgParser._addExtensionCommandsToParser(subParsers);
|
||||
|
||||
// add the 'setup' command
|
||||
ArgParser._addSetupToParser(subParsers);
|
||||
|
||||
// backwards compatibility / drop-in wrapper
|
||||
/**
|
||||
* @type {ArgParser['parseArgs']}
|
||||
@@ -205,7 +208,8 @@ class ArgParser {
|
||||
static _addServerToParser(subParser) {
|
||||
const serverParser = subParser.add_parser('server', {
|
||||
add_help: true,
|
||||
help: 'Run an Appium server',
|
||||
help: 'Start an Appium server',
|
||||
description: 'Start an Appium server (the "server" subcommand is optional)',
|
||||
});
|
||||
|
||||
ArgParser._patchExit(serverParser);
|
||||
@@ -227,7 +231,8 @@ class ArgParser {
|
||||
for (const type of /** @type {[DriverType, PluginType]} */ ([DRIVER_TYPE, PLUGIN_TYPE])) {
|
||||
const extParser = subParsers.add_parser(type, {
|
||||
add_help: true,
|
||||
help: `Access the ${type} management CLI commands`,
|
||||
help: `Manage Appium ${type}s`,
|
||||
description: `Manage Appium ${type}s using various subcommands`,
|
||||
});
|
||||
|
||||
ArgParser._patchExit(extParser);
|
||||
@@ -259,19 +264,17 @@ class ArgParser {
|
||||
{
|
||||
command: EXT_SUBCOMMAND_UPDATE,
|
||||
args: extensionArgs[type].update,
|
||||
help: `Update installed ${type}s to the latest version`,
|
||||
help: `Update one or more installed ${type}s to the latest version`,
|
||||
},
|
||||
{
|
||||
command: EXT_SUBCOMMAND_RUN,
|
||||
args: extensionArgs[type].run,
|
||||
help:
|
||||
`Run a script (defined inside the ${type}'s package.json under the ` +
|
||||
`“scripts” field inside the “appium” field) from an installed ${type}`,
|
||||
help: `Run a script (if available) from the given ${type}`,
|
||||
},
|
||||
{
|
||||
command: EXT_SUBCOMMAND_DOCTOR,
|
||||
args: extensionArgs[type].doctor,
|
||||
help: `Run doctor checks (if any defined) for the given ${type}`,
|
||||
help: `Run doctor checks (if available) for the given ${type}`,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -299,9 +302,12 @@ class ArgParser {
|
||||
static _addSetupToParser(subParser) {
|
||||
const setupParser = subParser.add_parser('setup', {
|
||||
add_help: true,
|
||||
help: `Select a preset of official drivers/plugins to install ` +
|
||||
`compatible with '${determinePlatformName()}' host platform. ` +
|
||||
`Existing drivers/plugins will remain. The default preset is 'mobile'.`,
|
||||
help: 'Batch install or uninstall Appium drivers and plugins',
|
||||
description:
|
||||
`Install a preset of official drivers/plugins compatible with the current host platform ` +
|
||||
`(${determinePlatformName()}). Existing drivers/plugins will remain. The default preset ` +
|
||||
`is "mobile". Providing the special "reset" subcommand will instead uninstall all ` +
|
||||
`drivers and plugins, and remove their related manifest files.`,
|
||||
});
|
||||
|
||||
|
||||
@@ -313,15 +319,25 @@ class ArgParser {
|
||||
const parserSpecs = [
|
||||
{
|
||||
command: SUBCOMMAND_MOBILE,
|
||||
help: `The preset for mobile devices: ${_.join(getPresetDrivers(SUBCOMMAND_MOBILE), ',')}`
|
||||
help:
|
||||
`The preset for mobile devices ` +
|
||||
`(drivers: ${_.join(getPresetDrivers(SUBCOMMAND_MOBILE), ',')}; plugins: ${DEFAULT_PLUGINS})`
|
||||
},
|
||||
{
|
||||
command: SUBCOMMAND_BROWSER,
|
||||
help: `The preset for desktop browser drivers: ${_.join(getPresetDrivers(SUBCOMMAND_BROWSER), ',')}`
|
||||
help:
|
||||
`The preset for desktop browsers ` +
|
||||
`(drivers: ${_.join(getPresetDrivers(SUBCOMMAND_BROWSER), ',')}; plugins: ${DEFAULT_PLUGINS})`
|
||||
},
|
||||
{
|
||||
command: SUBCOMMAND_DESKTOP,
|
||||
help: `The preset for desktop application drivers: ${_.join(getPresetDrivers(SUBCOMMAND_DESKTOP), ',')}`
|
||||
help:
|
||||
`The preset for desktop applications ` +
|
||||
`(drivers: ${_.join(getPresetDrivers(SUBCOMMAND_DESKTOP), ',')}; plugins: ${DEFAULT_PLUGINS})`
|
||||
},
|
||||
{
|
||||
command: SUBCOMMAND_RESET,
|
||||
help: 'Remove all installed drivers and plugins'
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -27,7 +27,9 @@ export function errAndQuit(json, msg) {
|
||||
* @param {string} msg - string to log
|
||||
*/
|
||||
export function log(json, msg) {
|
||||
!json && console.log(msg);
|
||||
if (!json) {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@ import _ from 'lodash';
|
||||
import {system, fs, npm} from '@appium/support';
|
||||
import axios from 'axios';
|
||||
import {exec} from 'teen_process';
|
||||
import semver from 'semver';
|
||||
import * as semver from 'semver';
|
||||
import os from 'node:os';
|
||||
import {npmPackage} from './utils';
|
||||
import B from 'bluebird';
|
||||
@@ -115,7 +115,7 @@ export async function getGitRev(useGithubApiFallback = false) {
|
||||
cwd: __dirname,
|
||||
});
|
||||
return stdout.trim();
|
||||
} catch (ign) {}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!useGithubApiFallback) {
|
||||
@@ -132,7 +132,7 @@ export async function getGitRev(useGithubApiFallback = false) {
|
||||
},
|
||||
})
|
||||
).data?.object?.sha;
|
||||
} catch (ign) {}
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ async function getGitTimestamp(commitSha, useGithubApiFallback = false) {
|
||||
cwd: __dirname,
|
||||
});
|
||||
return stdout.trim();
|
||||
} catch (ign) {}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!useGithubApiFallback) {
|
||||
@@ -164,7 +164,7 @@ async function getGitTimestamp(commitSha, useGithubApiFallback = false) {
|
||||
},
|
||||
})
|
||||
).data?.tagger?.date;
|
||||
} catch (ign) {}
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ export function checkNodeOk() {
|
||||
|
||||
export async function showBuildInfo() {
|
||||
await updateBuildInfo(true);
|
||||
console.log(JSON.stringify(getBuildInfo())); // eslint-disable-line no-console
|
||||
console.log(JSON.stringify(getBuildInfo()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,7 +356,7 @@ export async function requireDir(root, requireWriteable = true, displayName = 'f
|
||||
if (requireWriteable) {
|
||||
try {
|
||||
await fs.access(root, fs.constants.W_OK);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
throw new Error(
|
||||
`The ${displayName} '${root}' must be ` +
|
||||
`writeable for the current user account '${os.userInfo().username}'`
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '../schema/schema';
|
||||
import { pathToFileURL } from 'url';
|
||||
|
||||
const DEFAULT_ENTRY_POINT = 'index.js';
|
||||
/**
|
||||
* "npm" install type
|
||||
* Used when extension was installed by npm package name
|
||||
@@ -567,23 +568,39 @@ export class ExtensionConfig {
|
||||
const {mainClass} = this.installedExtensions[extName];
|
||||
const moduleRoot = this.getInstallPath(extName);
|
||||
const packageJsonPath = path.join(moduleRoot, 'package.json');
|
||||
let entryPointPath;
|
||||
let extensionManifest;
|
||||
try {
|
||||
entryPointPath = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')).main ?? 'index.js';
|
||||
extensionManifest = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
||||
} catch (e) {
|
||||
throw new ReferenceError(`Could not find a ${this.extensionType} installed at ${moduleRoot}`);
|
||||
throw new ReferenceError(
|
||||
`Could not read the ${this.extensionType} manifest at ${packageJsonPath}: ${e.message}`
|
||||
);
|
||||
}
|
||||
if (entryPointPath) {
|
||||
entryPointPath = path.resolve(moduleRoot, entryPointPath);
|
||||
} else {
|
||||
throw new ReferenceError(`Cannot find a valid ${this.extensionType} main entry point in '${packageJsonPath}'`);
|
||||
/** @type {string | undefined} */
|
||||
let entryPointRelativePath;
|
||||
try {
|
||||
if (extensionManifest.type === 'module' && extensionManifest.exports) {
|
||||
entryPointRelativePath = resolveEsmEntryPoint(extensionManifest.exports);
|
||||
}
|
||||
entryPointRelativePath = entryPointRelativePath ?? extensionManifest.main ?? DEFAULT_ENTRY_POINT;
|
||||
} catch (e) {
|
||||
throw new ReferenceError(
|
||||
`Could not find the ${this.extensionType} installed at ${moduleRoot}: ${e.message}`
|
||||
);
|
||||
}
|
||||
const entryPointFullPath = path.resolve(moduleRoot, /** @type {string} */ (entryPointRelativePath));
|
||||
if (!await fs.exists(entryPointFullPath)) {
|
||||
throw new ReferenceError(
|
||||
`Cannot find a valid ${this.extensionType} main entry point in '${packageJsonPath}'. ` +
|
||||
`Assumed entry point: '${entryPointFullPath}'`
|
||||
);
|
||||
}
|
||||
// note: this will only reload the entry point
|
||||
if (process.env.APPIUM_RELOAD_EXTENSIONS && require.cache[entryPointPath]) {
|
||||
log.debug(`Removing ${entryPointPath} from require cache`);
|
||||
delete require.cache[entryPointPath];
|
||||
if (process.env.APPIUM_RELOAD_EXTENSIONS && require.cache[entryPointFullPath]) {
|
||||
log.debug(`Removing ${entryPointFullPath} from require cache`);
|
||||
delete require.cache[entryPointFullPath];
|
||||
}
|
||||
return [entryPointPath, mainClass];
|
||||
return [entryPointFullPath, mainClass];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -672,6 +689,27 @@ export class ExtensionConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* https://nodejs.org/api/packages.html#package-entry-points
|
||||
*
|
||||
* @param {any} exportsValue
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
export function resolveEsmEntryPoint(exportsValue) {
|
||||
if (_.isString(exportsValue) && exportsValue) {
|
||||
return exportsValue;
|
||||
}
|
||||
if (!_.isPlainObject(exportsValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of ['.', 'import']) {
|
||||
if (exportsValue[key]) {
|
||||
return resolveEsmEntryPoint(exportsValue[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An issue with the {@linkcode ExtManifest} for a particular extension.
|
||||
*
|
||||
|
||||
@@ -242,8 +242,8 @@ export class Manifest {
|
||||
* @type {InternalMetadata}
|
||||
*/
|
||||
const internal = {
|
||||
pkgName: pkgJson.name,
|
||||
version: pkgJson.version,
|
||||
pkgName: /** @type {string} */ (pkgJson.name),
|
||||
version: /** @type {string} */ (pkgJson.version),
|
||||
appiumVersion: pkgJson.peerDependencies?.appium,
|
||||
installType,
|
||||
installSpec: `${pkgJson.name}@${pkgJson.version}`,
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import Ajv from 'ajv';
|
||||
import {Ajv} from 'ajv';
|
||||
import addFormats from 'ajv-formats';
|
||||
import _ from 'lodash';
|
||||
import path from 'path';
|
||||
@@ -453,7 +453,7 @@ class AppiumSchema {
|
||||
let refSchema;
|
||||
try {
|
||||
refSchema = this.getSchema($ref);
|
||||
} catch (err) {
|
||||
} catch {
|
||||
// this can happen if an extension schema supplies a $ref to a non-existent schema
|
||||
throw new SchemaUnknownSchemaError($ref);
|
||||
}
|
||||
|
||||
@@ -247,10 +247,10 @@ export function adjustNodePath() {
|
||||
// ! so it could break (maybe, eventually).
|
||||
// See https://gist.github.com/branneman/8048520#7-the-hack
|
||||
// @ts-ignore see above comment
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
|
||||
require('module').Module._initPaths();
|
||||
return true;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "appium",
|
||||
"version": "2.13.1",
|
||||
"version": "2.16.0",
|
||||
"description": "Automation for Apps.",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -60,19 +60,14 @@
|
||||
"test:unit": "mocha \"./test/unit/**/*.spec.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/base-driver": "^9.13.1",
|
||||
"@appium/base-plugin": "^2.2.50",
|
||||
"@appium/docutils": "^1.0.26",
|
||||
"@appium/base-driver": "^9.16.0",
|
||||
"@appium/base-plugin": "^2.3.1",
|
||||
"@appium/docutils": "^1.0.30",
|
||||
"@appium/logger": "^1.6.1",
|
||||
"@appium/schema": "^0.7.0",
|
||||
"@appium/support": "^6.0.0",
|
||||
"@appium/types": "^0.22.3",
|
||||
"@appium/schema": "^0.8.0",
|
||||
"@appium/support": "^6.0.4",
|
||||
"@appium/types": "^0.25.0",
|
||||
"@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",
|
||||
@@ -80,21 +75,20 @@
|
||||
"asyncbox": "3.0.0",
|
||||
"axios": "1.7.9",
|
||||
"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.2",
|
||||
"type-fest": "4.30.0",
|
||||
"teen_process": "2.2.3",
|
||||
"type-fest": "4.35.0",
|
||||
"winston": "3.17.0",
|
||||
"wrap-ansi": "7.0.0",
|
||||
"ws": "8.18.0",
|
||||
"yaml": "2.6.1"
|
||||
"yaml": "2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"webdriverio": "8.40.6"
|
||||
"webdriverio": "9.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,10 +85,10 @@ async function init() {
|
||||
}
|
||||
try {
|
||||
({env, util, logger} = require('@appium/support'));
|
||||
// @ts-ignore
|
||||
// @ts-ignore This is OK
|
||||
({runExtensionCommand} = require('../build/lib/cli/extension'));
|
||||
({DRIVER_TYPE, PLUGIN_TYPE} = require('../build/lib/constants'));
|
||||
// @ts-ignore
|
||||
// @ts-ignore This is OK
|
||||
({loadExtensions} = require('../build/lib/extension'));
|
||||
logger.getLogger('Appium').level = 'error';
|
||||
|
||||
@@ -119,7 +119,7 @@ async function main() {
|
||||
|
||||
if (!driverEnv && !pluginEnv) {
|
||||
spinner.succeed(
|
||||
wrap(`No drivers or plugins to automatically install.
|
||||
wrap(`No drivers or plugins to automatically install.
|
||||
If desired, provide arguments with comma-separated values "--drivers=<known_driver>[,known_driver...]" and/or "--plugins=<known_plugin>[,known_plugin...]" to the "npm install appium" command. The specified extensions will be installed automatically with Appium. Note: to see the list of known extensions, run "appium <driver|plugin> list".`)
|
||||
);
|
||||
return;
|
||||
@@ -193,7 +193,7 @@ async function checkAndInstallExtension({
|
||||
spinner,
|
||||
}) {
|
||||
const extList = await runExtensionCommand(
|
||||
// @ts-ignore
|
||||
// @ts-ignore This is OK
|
||||
{
|
||||
appiumHome,
|
||||
subcommand: type,
|
||||
@@ -209,7 +209,7 @@ async function checkAndInstallExtension({
|
||||
}
|
||||
spinner.start(`Installing ${type} "${ext}"...`);
|
||||
await runExtensionCommand(
|
||||
// @ts-ignore
|
||||
// @ts-ignore this is OK
|
||||
{
|
||||
subcommand: type,
|
||||
appiumHome,
|
||||
|
||||
@@ -3,12 +3,12 @@ import {tempDir, fs} from '@appium/support';
|
||||
import {exec} from 'teen_process';
|
||||
import B from 'bluebird';
|
||||
import {
|
||||
APPIUM_ROOT,
|
||||
readAppiumArgErrorFixture,
|
||||
formatAppiumArgErrorOutput,
|
||||
EXECUTABLE,
|
||||
runAppiumRaw,
|
||||
} from './e2e-helpers';
|
||||
import {APPIUM_ROOT} from '../helpers';
|
||||
import { stripColorCodes } from '../../lib/logsink';
|
||||
|
||||
describe('argument parsing', function () {
|
||||
|
||||
@@ -223,6 +223,11 @@ describe('Driver CLI', function () {
|
||||
});
|
||||
|
||||
it('should install a driver from GitHub', async function () {
|
||||
if (process.env.CI) {
|
||||
// This test is too slow for CI env
|
||||
return this.skip();
|
||||
}
|
||||
|
||||
const ret = await runInstall([
|
||||
'appium/appium-fake-driver',
|
||||
'--source',
|
||||
@@ -257,6 +262,11 @@ describe('Driver CLI', function () {
|
||||
});
|
||||
|
||||
it('should install a driver from a remote git repo', async function () {
|
||||
if (process.env.CI) {
|
||||
// This test is too slow for CI env
|
||||
return this.skip();
|
||||
}
|
||||
|
||||
const ret = await runInstall([
|
||||
'git+https://github.com/appium/appium-fake-driver.git',
|
||||
'--source',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {getGitRev, getBuildInfo, updateBuildInfo, APPIUM_VER} from '../../lib/config';
|
||||
import axios from 'axios';
|
||||
|
||||
@@ -4,9 +4,9 @@ import {BaseDriver} from '@appium/base-driver';
|
||||
import {exec} from 'teen_process';
|
||||
import {fs, tempDir} from '@appium/support';
|
||||
import axios from 'axios';
|
||||
import {command} from 'webdriver';
|
||||
import B from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {remote as wdio} from 'webdriverio';
|
||||
import {runExtensionCommand} from '../../lib/cli/extension';
|
||||
@@ -110,12 +110,14 @@ describe('FakeDriver via HTTP', function () {
|
||||
* @param {Partial<import('appium/types').ParsedArgs>} [args]
|
||||
*/
|
||||
function withServer(args = {}) {
|
||||
// eslint-disable-next-line mocha/no-sibling-hooks
|
||||
before(async function () {
|
||||
args = {...args, appiumHome, port, address: TEST_HOST};
|
||||
if (shouldStartServer) {
|
||||
server = await appiumServer(args);
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line mocha/no-sibling-hooks
|
||||
after(async function () {
|
||||
if (server) {
|
||||
await server.close();
|
||||
@@ -206,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();
|
||||
|
||||
@@ -251,8 +324,6 @@ describe('FakeDriver via HTTP', function () {
|
||||
|
||||
await B.delay(250);
|
||||
await driver.getPageSource().should.eventually.be.rejectedWith(/terminated/);
|
||||
|
||||
await driver.getSessions().should.eventually.be.empty;
|
||||
});
|
||||
|
||||
it('should not allow umbrella commands to prevent newCommandTimeout on inner driver', async function () {
|
||||
@@ -264,6 +335,10 @@ describe('FakeDriver via HTTP', function () {
|
||||
);
|
||||
let driver = await wdio({...wdOpts, capabilities: localCaps});
|
||||
should.exist(driver.sessionId);
|
||||
driver.addCommand(
|
||||
'getSessions',
|
||||
async () => (await axios.get(`${testServerBaseUrl}/sessions`)).data.value
|
||||
);
|
||||
|
||||
// get the session list 6 times over 300ms. each request will be below the new command
|
||||
// timeout but since they are not received by the driver the session should still time out
|
||||
@@ -506,51 +581,12 @@ describe('FakeDriver via HTTP', function () {
|
||||
await driver.deleteSession();
|
||||
}
|
||||
});
|
||||
|
||||
it.skip('should log a single deprecation warning if a deprecated method is used and not overridden by a newMethodMap', async function () {
|
||||
let driver = await wdio({...wdOpts, capabilities: caps});
|
||||
try {
|
||||
driver.addCommand(
|
||||
'deprecated',
|
||||
command('POST', '/session/:sessionId/deprecated', {
|
||||
command: 'deprecated',
|
||||
description: 'Call a deprecated command',
|
||||
parameters: [],
|
||||
ref: '',
|
||||
}),
|
||||
);
|
||||
driver.addCommand(
|
||||
'doubleClick',
|
||||
command('POST', '/session/:sessionId/doubleclick', {
|
||||
command: 'doubleClick',
|
||||
description: 'Global double click',
|
||||
parameters: [],
|
||||
ref: '',
|
||||
}),
|
||||
);
|
||||
await driver
|
||||
.executeScript('fake: getDeprecatedCommandsCalled', [])
|
||||
.should.eventually.eql([]);
|
||||
await driver.deprecated();
|
||||
await driver.deprecated();
|
||||
await driver.shake();
|
||||
|
||||
// this call should not trigger a deprecation even though deprecated by appium because it's
|
||||
// overridden as not deprecated by fake driver
|
||||
await driver.doubleClick();
|
||||
|
||||
await driver
|
||||
.executeScript('fake: getDeprecatedCommandsCalled', [])
|
||||
.should.eventually.eql(['callDeprecatedCommand', 'mobileShake']);
|
||||
} finally {
|
||||
await driver.deleteSession();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bidi protocol', function () {
|
||||
withServer();
|
||||
const capabilities = {...caps, webSocketUrl: true, 'appium:runClock': true};
|
||||
/** @type import('webdriverio').Browser **/
|
||||
let driver;
|
||||
|
||||
beforeEach(async function () {
|
||||
@@ -581,7 +617,7 @@ describe('FakeDriver via HTTP', function () {
|
||||
it('should be able to subscribe and unsubscribe to bidi events', async function () {
|
||||
let collectedEvents = [];
|
||||
// asyncrhonously start our listener
|
||||
driver.on('clock.currentTime', ({time}) => {
|
||||
driver.on('appium:clock.currentTime', ({time}) => {
|
||||
collectedEvents.push(time);
|
||||
});
|
||||
|
||||
@@ -591,16 +627,24 @@ describe('FakeDriver via HTTP', function () {
|
||||
collectedEvents.should.be.empty;
|
||||
|
||||
// now subscribe and wait and assert that some events have been collected
|
||||
await driver.sessionSubscribe({events: ['clock.currentTime']});
|
||||
await driver.sessionSubscribe({events: ['appium:clock.currentTime']});
|
||||
await B.delay(750);
|
||||
collectedEvents.should.not.be.empty;
|
||||
|
||||
// finally unsubscribe and wait and assert that some events have been collected
|
||||
await driver.sessionUnsubscribe({events: ['clock.currentTime']});
|
||||
await driver.sessionUnsubscribe({events: ['appium:clock.currentTime']});
|
||||
collectedEvents = [];
|
||||
await B.delay(750);
|
||||
collectedEvents.should.be.empty;
|
||||
});
|
||||
|
||||
it('should allow custom bidi commands', async function () {
|
||||
let {result} = await driver.send({method: 'appium:fake.getFakeThing', params: {}});
|
||||
should.not.exist(result);
|
||||
await driver.send({method: 'appium:fake.setFakeThing', params: {thing: 'this is from bidi'}});
|
||||
({result} = await driver.send({method: 'appium:fake.getFakeThing', params: {}}));
|
||||
result.should.eql('this is from bidi');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -633,15 +677,14 @@ describe('Bidi over SSL', function () {
|
||||
// since we update the FakeDriver.prototype below, make sure we update the FakeDriver which is
|
||||
// actually going to be required by Appium
|
||||
/** @type {import('@appium/types').DriverClass} */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
let FakeDriver;
|
||||
/** @type {string} */
|
||||
let testServerBaseSessionUrl;
|
||||
/** @type {import('sinon').SinonSandbox} */
|
||||
let sandbox;
|
||||
let certPath = 'certificate.cert';
|
||||
let keyPath = 'certificate.key';
|
||||
const capabilities = {...caps, webSocketUrl: true};
|
||||
let should;
|
||||
/** @type {string | undefined} */
|
||||
let previousEnvValue;
|
||||
|
||||
before(async function () {
|
||||
const chai = await import('chai');
|
||||
@@ -657,7 +700,6 @@ describe('Bidi over SSL', function () {
|
||||
}
|
||||
return this.skip();
|
||||
}
|
||||
sandbox = createSandbox();
|
||||
appiumHome = await tempDir.openDir();
|
||||
wdOpts.port = port = await getTestPort();
|
||||
testServerBaseUrl = `https://${TEST_HOST}:${port}`;
|
||||
@@ -672,15 +714,20 @@ describe('Bidi over SSL', function () {
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await fs.rimraf(appiumHome);
|
||||
await server.close();
|
||||
if (server) {
|
||||
await fs.rimraf(appiumHome);
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
previousEnvValue = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
driver = await wdio({...wdOpts, protocol: 'https', strictSSL: false, capabilities});
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = previousEnvValue;
|
||||
if (driver) {
|
||||
await driver.deleteSession();
|
||||
}
|
||||
|
||||
@@ -28,11 +28,32 @@ const wdOpts = {
|
||||
capabilities: W3C_PREFIXED_CAPS,
|
||||
};
|
||||
|
||||
/** @type {Partial<import('appium/types').ParsedArgs>} */
|
||||
let baseServerArgs;
|
||||
|
||||
/**
|
||||
* @param {Partial<import('appium/types').ParsedArgs>} args
|
||||
*/
|
||||
function serverSetup(args) {
|
||||
/** @type {AppiumServer|null} */
|
||||
let server = null;
|
||||
|
||||
before(async function () { // eslint-disable-line mocha/no-top-level-hooks
|
||||
// then start server if we need to
|
||||
server = /** @type {AppiumServer} */ (
|
||||
await appiumServer({...baseServerArgs, ...args})
|
||||
);
|
||||
});
|
||||
after(async function () { // eslint-disable-line mocha/no-top-level-hooks
|
||||
if (server) {
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe('FakePlugin w/ FakeDriver via HTTP', function () {
|
||||
/** @type {string} */
|
||||
let appiumHome;
|
||||
/** @type {Partial<import('appium/types').ParsedArgs>} */
|
||||
let baseServerArgs;
|
||||
/** @type {string} */
|
||||
let testServerBaseUrl;
|
||||
/** @type {number} */
|
||||
@@ -61,7 +82,7 @@ describe('FakePlugin w/ FakeDriver via HTTP', function () {
|
||||
subcommand: DRIVER_TYPE,
|
||||
suppressOutput: true,
|
||||
},
|
||||
driverConfig
|
||||
driverConfig,
|
||||
);
|
||||
if (!('fake' in driverList)) {
|
||||
await runExtensionCommand(
|
||||
@@ -71,7 +92,7 @@ describe('FakePlugin w/ FakeDriver via HTTP', function () {
|
||||
installType: INSTALL_TYPE_LOCAL,
|
||||
subcommand: DRIVER_TYPE,
|
||||
},
|
||||
driverConfig
|
||||
driverConfig,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,7 +104,7 @@ describe('FakePlugin w/ FakeDriver via HTTP', function () {
|
||||
json: true,
|
||||
suppressOutput: true,
|
||||
},
|
||||
pluginConfig
|
||||
pluginConfig,
|
||||
);
|
||||
if (!('fake' in pluginList)) {
|
||||
await runExtensionCommand(
|
||||
@@ -93,7 +114,7 @@ describe('FakePlugin w/ FakeDriver via HTTP', function () {
|
||||
plugin: FAKE_PLUGIN_DIR,
|
||||
installType: INSTALL_TYPE_LOCAL,
|
||||
},
|
||||
pluginConfig
|
||||
pluginConfig,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,27 +154,9 @@ describe('FakePlugin w/ FakeDriver via HTTP', function () {
|
||||
|
||||
for (const registrationType of ['explicit', 'all']) {
|
||||
describe(`with plugin registered via type ${registrationType}`, function () {
|
||||
/** @type {AppiumServer} */
|
||||
let server;
|
||||
/** @type {import('type-fest').LiteralUnion<'all', string>[]} */
|
||||
let usePlugins;
|
||||
before(async function () {
|
||||
// then start server if we need to
|
||||
usePlugins = registrationType === 'explicit' ? ['fake'] : ['all'];
|
||||
const args = {
|
||||
appiumHome,
|
||||
port,
|
||||
address: TEST_HOST,
|
||||
usePlugins,
|
||||
useDrivers: ['fake'],
|
||||
};
|
||||
server = /** @type {AppiumServer} */ (await appiumServer(args));
|
||||
});
|
||||
after(async function () {
|
||||
if (server) {
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
const usePlugins = registrationType === 'explicit' ? ['fake'] : ['all'];
|
||||
serverSetup({usePlugins});
|
||||
it('should update the server', async function () {
|
||||
const res = {fake: 'fakeResponse'};
|
||||
(await axios.post(`http://${TEST_HOST}:${port}/fake`)).data.should.eql(res);
|
||||
@@ -369,6 +372,91 @@ describe('FakePlugin w/ FakeDriver via HTTP', function () {
|
||||
sum.should.eql(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BiDi support', function () {
|
||||
describe('with a single plugin', function() {
|
||||
/** @type {import('webdriverio').Browser} */
|
||||
let driver;
|
||||
|
||||
// this 'after' block needs to come before 'serverSetup' so that the delete session happens
|
||||
// before the server shutdown
|
||||
after(async function () {
|
||||
if (driver) {
|
||||
await driver.deleteSession();
|
||||
}
|
||||
});
|
||||
|
||||
serverSetup({});
|
||||
|
||||
before(async function () {
|
||||
const caps = {...wdOpts.capabilities, webSocketUrl: true, 'appium:runClock': true};
|
||||
driver = await wdio({...wdOpts, capabilities: caps});
|
||||
});
|
||||
|
||||
it('should handle custom bidi commands if registered', async function () {
|
||||
let {result} = await driver.send({method: 'appium:fake.getPluginThing', params: {}});
|
||||
should.not.exist(result);
|
||||
await driver.send({method: 'appium:fake.setPluginThing', params: {thing: 'plugin bidi'}});
|
||||
({result} = await driver.send({method: 'appium:fake.getPluginThing', params: {}}));
|
||||
result.should.eql('plugin bidi');
|
||||
});
|
||||
|
||||
it('should subscribe and unsubscribe to/from custom bidi events', async function () {
|
||||
let retrievals = 0;
|
||||
// asyncrhonously start our listener
|
||||
driver.on('appium:fake.pluginThingRetrieved', () => {
|
||||
retrievals++;
|
||||
});
|
||||
|
||||
await driver.send({method: 'appium:fake.getPluginThing', params: {}});
|
||||
retrievals.should.eql(0);
|
||||
|
||||
await driver.sessionSubscribe({events: ['appium:fake.pluginThingRetrieved']});
|
||||
await driver.send({method: 'appium:fake.getPluginThing', params: {}});
|
||||
await driver.send({method: 'appium:fake.getPluginThing', params: {}});
|
||||
retrievals.should.eql(2);
|
||||
|
||||
await driver.sessionUnsubscribe({events: ['appium:fake.pluginThingRetrieved']});
|
||||
await driver.send({method: 'appium:fake.getPluginThing', params: {}});
|
||||
await driver.send({method: 'appium:fake.getPluginThing', params: {}});
|
||||
retrievals.should.eql(2);
|
||||
});
|
||||
|
||||
it('should subscribe and unsubscribe to/from custom bidi events and merge with driver', async function () {
|
||||
let collectedEvents = [];
|
||||
// asyncrhonously start our listener
|
||||
driver.on('appium:clock.currentTime', ({time}) => {
|
||||
collectedEvents.push(time);
|
||||
});
|
||||
|
||||
// wait for some time to be sure clock events have happened, and assert we don't receive
|
||||
// any yet
|
||||
await B.delay(750);
|
||||
collectedEvents.should.be.empty;
|
||||
|
||||
// now subscribe and wait and assert that some events have been collected
|
||||
await driver.sessionSubscribe({events: ['appium:clock.currentTime']});
|
||||
await B.delay(800);
|
||||
collectedEvents.length.should.eql(5);
|
||||
|
||||
// finally unsubscribe and wait and assert that some events have been collected
|
||||
await driver.sessionUnsubscribe({events: ['appium:clock.currentTime']});
|
||||
collectedEvents = [];
|
||||
await B.delay(800);
|
||||
collectedEvents.should.be.empty;
|
||||
});
|
||||
|
||||
it('should call underlying driver bidi method if next is called', async function () {
|
||||
const {result} = await driver.send({method: 'appium:fake.doSomeMath', params: {num1: 2, num2: 3}});
|
||||
result.should.eql(11);
|
||||
});
|
||||
|
||||
it('should override and not call underlying driver bidi method if next is not called', async function () {
|
||||
const {result} = await driver.send({method: 'appium:fake.doSomeMath2', params: {num1: 2, num2: 3}});
|
||||
result.should.eql(6);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,7 @@ export default {
|
||||
denyInsecure: [],
|
||||
driversImportChunkSize: 3,
|
||||
keepAliveTimeout: 600,
|
||||
requestTimeout: 3600,
|
||||
localTimezone: false,
|
||||
logFormat: 'text',
|
||||
loglevel: 'debug',
|
||||
|
||||
@@ -6,11 +6,12 @@ import {BaseDriver} from '@appium/base-driver';
|
||||
import {FakeDriver} from '@appium/fake-driver';
|
||||
import {sleep} from 'asyncbox';
|
||||
import _ from 'lodash';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {finalizeSchema, registerSchema, resetSchema} from '../../lib/schema/schema';
|
||||
import {insertAppiumPrefixes, removeAppiumPrefixes} from '../../lib/utils';
|
||||
import {rewiremock, BASE_CAPS, W3C_CAPS, W3C_PREFIXED_CAPS} from '../helpers';
|
||||
import BasePlugin from '@appium/base-plugin';
|
||||
import {BasePlugin} from '@appium/base-plugin';
|
||||
|
||||
const SESSION_ID = '1';
|
||||
|
||||
@@ -358,7 +359,7 @@ describe('AppiumDriver', function () {
|
||||
_.keys(appium.sessions).should.not.contain(sessionId);
|
||||
});
|
||||
it('should remove session if inner driver unexpectedly exits with no error', async function () {
|
||||
let [sessionId] = (await appium.createSession(null, null, _.clone(W3C_CAPS))).value; // eslint-disable-line comma-spacing
|
||||
let [sessionId] = (await appium.createSession(null, null, _.clone(W3C_CAPS))).value;
|
||||
_.keys(appium.sessions).should.contain(sessionId);
|
||||
appium.sessions[sessionId].eventEmitter.emit('onUnexpectedShutdown');
|
||||
// let event loop spin so rejection is handled
|
||||
|
||||
@@ -2,6 +2,7 @@ import {tempDir, fs, npm} from '@appium/support';
|
||||
import {loadExtensions} from '../../../lib/extension';
|
||||
import {Manifest} from '../../../lib/extension/manifest';
|
||||
import DriverCommand from '../../../lib/cli/driver-command';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
|
||||
describe('DriverCommand', function () {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {finalizeSchema, resetSchema, SchemaFinalizationError} from '../../../lib/schema/schema';
|
||||
import {rewiremock} from '../../helpers';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {system} from '@appium/support';
|
||||
import {getPresetDrivers} from '../../../lib/cli/setup-command';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-check
|
||||
|
||||
import fs from 'fs';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import YAML from 'yaml';
|
||||
import * as schema from '../../lib/schema/schema';
|
||||
@@ -212,14 +213,11 @@ describe('config-file', function () {
|
||||
});
|
||||
|
||||
describe('when the config file is invalid', function () {
|
||||
beforeEach(function () {
|
||||
beforeEach(async function () {
|
||||
lc.search.resolves({
|
||||
config: {foo: 'bar'},
|
||||
filepath: '/path/to/file.json',
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
result = await readConfigFile();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-check
|
||||
|
||||
import _ from 'lodash';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {getParser} from '../../lib/cli/parser';
|
||||
import {
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from 'path';
|
||||
import {version as APPIUM_VER} from '../../../package.json';
|
||||
import {FAKE_DRIVER_DIR, PROJECT_ROOT, rewiremock} from '../../helpers';
|
||||
import {initMocks} from './mocks';
|
||||
import {resolveEsmEntryPoint} from '../../../lib/extension/extension-config';
|
||||
|
||||
describe('ExtensionConfig', function () {
|
||||
/** @type {import('sinon').SinonSandbox} */
|
||||
@@ -45,7 +46,31 @@ describe('ExtensionConfig', function () {
|
||||
process.removeAllListeners('exit');
|
||||
});
|
||||
|
||||
describe('constructor', function () {});
|
||||
describe('ESM module resolution', function () {
|
||||
it('resolves ESM entry point with simple export', function () {
|
||||
resolveEsmEntryPoint('./index.js').should.eql('./index.js');
|
||||
});
|
||||
|
||||
it('resolves ESM entry point with dot export', function () {
|
||||
resolveEsmEntryPoint({
|
||||
'.': './index.js'
|
||||
}).should.eql('./index.js');
|
||||
});
|
||||
|
||||
it('resolves ESM entry point with import export', function () {
|
||||
resolveEsmEntryPoint({
|
||||
import: './index.js'
|
||||
}).should.eql('./index.js');
|
||||
});
|
||||
|
||||
it('resolves ESM entry point with complex import export', function () {
|
||||
resolveEsmEntryPoint({
|
||||
'.': {
|
||||
import: './index.js'
|
||||
}
|
||||
}).should.eql('./index.js');
|
||||
});
|
||||
});
|
||||
|
||||
describe('instance method', function () {
|
||||
/** @type {import('appium/lib/extension/extension-config').ExtensionConfig<DriverType>} */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable require-await */
|
||||
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import {EventEmitter} from 'events';
|
||||
import path from 'path';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {version as APPIUM_VER} from '../../../package.json';
|
||||
|
||||
@@ -34,6 +35,7 @@ export function initMocks(sandbox = createSandbox()) {
|
||||
findRoot: /** @type {MockAppiumSupportFs['findRoot']} */ (
|
||||
sandbox.stub().returns(path.join(__dirname, '..', '..', '..'))
|
||||
),
|
||||
exists: /** @type {MockAppiumSupportFs['exists']} */ (sandbox.stub().resolves(true)),
|
||||
},
|
||||
env: {
|
||||
resolveAppiumHome: /** @type {MockAppiumSupportEnv['resolveAppiumHome']} */ (
|
||||
@@ -171,6 +173,7 @@ export function initMocks(sandbox = createSandbox()) {
|
||||
* @property {SinonStubbedMember<SupportFs['mkdirp']>} mkdirp
|
||||
* @property {SinonStubbedMember<SupportFs['readPackageJsonFrom']>} readPackageJsonFrom
|
||||
* @property {SinonStubbedMember<SupportFs['findRoot']>} findRoot
|
||||
* @property {SinonStubbedMember<SupportFs['exists']>} exists
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @ts-check
|
||||
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {rewiremock} from '../helpers';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {init as logsinkInit, clear as logsinkClear} from '../../lib/logsink';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {logger} from '@appium/support';
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// @ts-check
|
||||
|
||||
import _ from 'lodash';
|
||||
import {PLUGIN_TYPE} from '../../../lib/constants';
|
||||
import {finalizeSchema, registerSchema, resetSchema} from '../../../lib/schema';
|
||||
@@ -305,10 +303,6 @@ describe('cli-args', function () {
|
||||
/`enum` is only supported for `type: 'string'`/i
|
||||
);
|
||||
});
|
||||
|
||||
it(
|
||||
'should actually throw earlier by failing schema validation, but that would mean overriding the behavior of `enum` which sounds inadvisable'
|
||||
);
|
||||
});
|
||||
|
||||
describe('when used with a `string` type', function () {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-check
|
||||
|
||||
import _ from 'lodash';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {DRIVER_TYPE, PLUGIN_TYPE} from '../../../lib/constants';
|
||||
import {AppiumConfigJsonSchema} from '@appium/schema';
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import {BASE_CAPS, W3C_CAPS} from '../helpers';
|
||||
import _ from 'lodash';
|
||||
import {stripColors} from '@colors/colors';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import logger from '../../lib/logger';
|
||||
import {fs} from '@appium/support';
|
||||
|
||||
@@ -3,6 +3,67 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **appium,base-driver,base-plugin:** allow plugins to define custom bidi commands and emit bidi events ([#20876](https://github.com/appium/appium/issues/20876)) ([8df1c21](https://github.com/appium/appium/commit/8df1c217a15d30300c04b9f59cdbdffa70325828))
|
||||
|
||||
|
||||
|
||||
## [9.14.1](https://github.com/appium/appium/compare/@appium/base-driver@9.14.0...@appium/base-driver@9.14.1) (2025-01-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **driver-test-support:** update dependency @types/lodash to v4.17.14 ([#20877](https://github.com/appium/appium/issues/20877)) ([fe209ed](https://github.com/appium/appium/commit/fe209ed660e0f790c98cd51938d0c75712cfd8e4))
|
||||
|
||||
|
||||
|
||||
## [9.14.0](https://github.com/appium/appium/compare/@appium/base-driver@9.13.1...@appium/base-driver@9.14.0) (2025-01-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **appium:** Add session.status BiDi command ([#20839](https://github.com/appium/appium/issues/20839)) ([64e768e](https://github.com/appium/appium/commit/64e768efb7bebd6b5a24d55206d1cad00812777c))
|
||||
* **appium:** allow drivers to define their own bidi commands ([#20828](https://github.com/appium/appium/issues/20828)) ([a917ec6](https://github.com/appium/appium/commit/a917ec6ceda2166fb3dcbff6b2768f700db9e103))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **base-driver:** Return an empty object if the corresponding API response is undefined ([#20845](https://github.com/appium/appium/issues/20845)) ([c765bc3](https://github.com/appium/appium/commit/c765bc31e03d37ed194fcd52f4e4a2ca1128b260))
|
||||
* **base-driver:** update dependency express to v4.21.2 ([#20823](https://github.com/appium/appium/issues/20823)) ([c032352](https://github.com/appium/appium/commit/c032352fade2146cdce5feb0906726ad07a30e4f))
|
||||
* Reduce linter warnings ([#20860](https://github.com/appium/appium/issues/20860)) ([65658cc](https://github.com/appium/appium/commit/65658ccbdde9144c45cb5aad6a9089a5d6f3a0a3))
|
||||
* **types:** update dependency type-fest to v4 ([#20838](https://github.com/appium/appium/issues/20838)) ([a5897dd](https://github.com/appium/appium/commit/a5897dd25a277a42b0c650a52274ba2c891ac3b0))
|
||||
* **types:** update dependency type-fest to v4 ([#20843](https://github.com/appium/appium/issues/20843)) ([7abecad](https://github.com/appium/appium/commit/7abecaddd3ed64c7be321650b2a17990e74a7222))
|
||||
* **types:** update dependency type-fest to v4.31.0 ([#20857](https://github.com/appium/appium/issues/20857)) ([24abb38](https://github.com/appium/appium/commit/24abb385e54f57457c4fb3f2b654cb63645e7ccd))
|
||||
|
||||
|
||||
|
||||
## [9.13.1](https://github.com/appium/appium/compare/@appium/base-driver@9.13.0...@appium/base-driver@9.13.1) (2024-12-05)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {Constraints, Driver, IBidiCommands} from '@appium/types';
|
||||
import type {Constraints, DriverStatus, IBidiCommands} from '@appium/types';
|
||||
import type {BaseDriver} from '../driver';
|
||||
import {mixin} from './mixin';
|
||||
import _ from 'lodash';
|
||||
|
||||
declare module '../driver' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -32,6 +33,19 @@ const BidiCommands: IBidiCommands = {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async bidiStatus<C extends Constraints>(this: BaseDriver<C>): Promise<DriverStatus> {
|
||||
const result = await this.getStatus();
|
||||
if (!_.has(result, 'ready')) {
|
||||
//@ts-ignore This is OK
|
||||
result.ready = true;
|
||||
}
|
||||
if (!_.has(result, 'message')) {
|
||||
//@ts-ignore This is OK
|
||||
result.message = `${this.constructor.name} is ready to accept commands`;
|
||||
}
|
||||
return result as DriverStatus;
|
||||
}
|
||||
};
|
||||
|
||||
mixin(BidiCommands);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5,7 +5,7 @@ import {BaseDriver} from '../driver';
|
||||
import {mixin} from './mixin';
|
||||
|
||||
declare module '../driver' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
interface BaseDriver<C extends Constraints> extends IFindCommands {}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,5 @@ import {BaseDriver} from '../driver';
|
||||
* @param mixin Mixin implementation
|
||||
*/
|
||||
export function mixin<C extends Constraints, T extends Partial<BaseDriver<C>>>(mixin: T): void {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
Object.assign(BaseDriver.prototype, mixin);
|
||||
}
|
||||
|
||||
@@ -76,19 +76,23 @@ const TimeoutCommands: ITimeoutCommands = {
|
||||
},
|
||||
|
||||
// pageLoad
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async pageLoadTimeoutW3C<C extends Constraints>(this: BaseDriver<C>, ms) {
|
||||
throw new errors.NotImplementedError('Not implemented yet for pageLoad.');
|
||||
},
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async pageLoadTimeoutMJSONWP<C extends Constraints>(this: BaseDriver<C>, ms) {
|
||||
throw new errors.NotImplementedError('Not implemented yet for pageLoad.');
|
||||
},
|
||||
|
||||
// script
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async scriptTimeoutW3C<C extends Constraints>(this: BaseDriver<C>, ms) {
|
||||
throw new errors.NotImplementedError('Not implemented yet for script.');
|
||||
},
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async scriptTimeoutMJSONWP<C extends Constraints>(this: BaseDriver<C>, ms) {
|
||||
throw new errors.NotImplementedError('Not implemented yet for script.');
|
||||
},
|
||||
@@ -99,7 +103,7 @@ const TimeoutCommands: ITimeoutCommands = {
|
||||
},
|
||||
|
||||
setImplicitWait<C extends Constraints>(this: BaseDriver<C>, ms: number) {
|
||||
// eslint-disable-line require-await
|
||||
|
||||
this.implicitWaitMs = ms;
|
||||
this.log.debug(`Set implicit wait to ${ms}ms`);
|
||||
if (this.managedDrivers && this.managedDrivers.length) {
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable require-await */
|
||||
|
||||
import {logger} from '@appium/support';
|
||||
import type {
|
||||
AppiumLogger,
|
||||
Constraints,
|
||||
@@ -14,21 +10,18 @@ import type {
|
||||
Protocol,
|
||||
RouteMatcher,
|
||||
StringRecord,
|
||||
BidiMethodDef,
|
||||
} from '@appium/types';
|
||||
import AsyncLock from 'async-lock';
|
||||
import _ from 'lodash';
|
||||
import {EventEmitter} from 'node:events';
|
||||
import os from 'node:os';
|
||||
import {
|
||||
DEFAULT_BASE_PATH,
|
||||
PROTOCOLS,
|
||||
MAX_LOG_BODY_LENGTH,
|
||||
} from '../constants';
|
||||
import {errors} from '../protocol';
|
||||
import DeviceSettings from './device-settings';
|
||||
import helpers, {BASEDRIVER_VER} from './helpers';
|
||||
import {BIDI_COMMANDS} from '../protocol/bidi-commands';
|
||||
import {DeviceSettings} from './device-settings';
|
||||
import * as helpers from './helpers';
|
||||
import {ExtensionCore} from './extension-core';
|
||||
|
||||
const NEW_COMMAND_TIMEOUT_MS = 60 * 1000;
|
||||
|
||||
@@ -38,13 +31,13 @@ const ALL_DRIVERS_MATCH = '*';
|
||||
const FEATURE_NAME_SEPARATOR = ':';
|
||||
|
||||
export class DriverCore<const C extends Constraints, Settings extends StringRecord = StringRecord>
|
||||
implements Core<C, Settings>
|
||||
extends ExtensionCore implements Core<C, Settings>
|
||||
{
|
||||
/**
|
||||
* Make the basedriver version available so for any driver which inherits from this package, we
|
||||
* know which version of basedriver it inherited from
|
||||
*/
|
||||
static baseVersion = BASEDRIVER_VER;
|
||||
static baseVersion = helpers.BASEDRIVER_VER;
|
||||
|
||||
sessionId: string | null;
|
||||
|
||||
@@ -84,11 +77,8 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
|
||||
|
||||
protected _eventHistory: EventHistory;
|
||||
|
||||
// used to handle driver events
|
||||
eventEmitter: NodeJS.EventEmitter;
|
||||
|
||||
/**
|
||||
* @privateRemarks XXX: unsure why this is wrapped in a getter when nothing else is
|
||||
* TODO: remove this._log and use this.log instead
|
||||
*/
|
||||
protected _log: AppiumLogger;
|
||||
|
||||
@@ -108,12 +98,10 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
|
||||
|
||||
protocol?: Protocol;
|
||||
|
||||
bidiEventSubs: Record<string, string[]>;
|
||||
|
||||
doesSupportBidi: boolean;
|
||||
|
||||
constructor(opts: InitialOpts = <InitialOpts>{}, shouldValidateCaps = true) {
|
||||
this._log = logger.getLogger(helpers.generateDriverLogPrefix(this as Core<C>));
|
||||
super();
|
||||
this._log = this.log; // TODO: remove references to this._log and use this.log instead
|
||||
|
||||
|
||||
// setup state
|
||||
this.opts = opts as DriverOpts<C>;
|
||||
@@ -141,16 +129,9 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
|
||||
this.managedDrivers = [];
|
||||
this.noCommandTimer = null;
|
||||
this._eventHistory = {commands: []};
|
||||
this.eventEmitter = new EventEmitter();
|
||||
this.shutdownUnexpectedly = false;
|
||||
this.commandsQueueGuard = new AsyncLock();
|
||||
this.settings = new DeviceSettings();
|
||||
this.bidiEventSubs = {};
|
||||
this.doesSupportBidi = false;
|
||||
}
|
||||
|
||||
get log() {
|
||||
return this._log;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -239,6 +220,7 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
|
||||
* method required by MJSONWP in order to determine if the command should
|
||||
* be proxied directly to the driver
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
driverForSession(sessionId: string): Core<Constraints> | null {
|
||||
return this as Core<Constraints>;
|
||||
}
|
||||
@@ -364,14 +346,17 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
|
||||
return null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
proxyActive(sessionId: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
getProxyAvoidList(sessionId: string): RouteMatcher[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
canProxy(sessionId: string): boolean {
|
||||
return false;
|
||||
}
|
||||
@@ -389,6 +374,7 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
|
||||
*
|
||||
* @returns whether the route should be avoided
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
proxyRouteIsAvoided(sessionId: string, method: HTTPMethod, url: string, body?: any): boolean {
|
||||
for (const avoidSchema of this.getProxyAvoidList(sessionId)) {
|
||||
if (!_.isArray(avoidSchema) || avoidSchema.length !== 2) {
|
||||
@@ -428,59 +414,4 @@ export class DriverCore<const C extends Constraints, Settings extends StringReco
|
||||
}
|
||||
}
|
||||
|
||||
async executeBidiCommand(bidiCmd: string, bidiParams: StringRecord): Promise<any> {
|
||||
const [moduleName, methodName] = bidiCmd.split('.');
|
||||
|
||||
// if we don't get a valid format for bidi command name, reject
|
||||
if (!moduleName || !methodName) {
|
||||
throw new errors.UnknownCommandError(
|
||||
`Did not receive a valid BiDi module and method name ` +
|
||||
`of the form moduleName.methodName. Instead received ` +
|
||||
`'${moduleName}.${methodName}'`,
|
||||
);
|
||||
}
|
||||
|
||||
// if the command module isn't part of our spec, reject
|
||||
if (!BIDI_COMMANDS[moduleName]) {
|
||||
throw new errors.UnknownCommandError();
|
||||
}
|
||||
|
||||
const {command, params} = BIDI_COMMANDS[moduleName][methodName] as BidiMethodDef;
|
||||
// if the command method isn't part of our spec, also reject
|
||||
if (!command) {
|
||||
throw new errors.UnknownCommandError();
|
||||
}
|
||||
|
||||
// If the driver doesn't have this command, it must not be implemented
|
||||
if (!this[command]) {
|
||||
throw new errors.NotYetImplementedError();
|
||||
}
|
||||
|
||||
// TODO improve param parsing and error messages along the lines of what we have in the http
|
||||
// handlers
|
||||
const args: any[] = [];
|
||||
if (params?.required?.length) {
|
||||
for (const requiredParam of params.required) {
|
||||
if (_.isUndefined(bidiParams[requiredParam])) {
|
||||
throw new errors.InvalidArgumentError(
|
||||
`The ${requiredParam} parameter was required but you omitted it`,
|
||||
);
|
||||
}
|
||||
args.push(bidiParams[requiredParam]);
|
||||
}
|
||||
}
|
||||
if (params?.optional?.length) {
|
||||
for (const optionalParam of params.optional) {
|
||||
args.push(bidiParams[optionalParam]);
|
||||
}
|
||||
}
|
||||
const logParams = _.truncate(JSON.stringify(bidiParams), {length: MAX_LOG_BODY_LENGTH});
|
||||
this.log.debug(
|
||||
`Executing bidi command '${bidiCmd}' with params ${logParams} by passing to driver ` +
|
||||
`method '${command}'`,
|
||||
);
|
||||
const res = (await this[command](...args)) ?? null;
|
||||
this.log.debug(`Responding to bidi command '${bidiCmd}' with ${JSON.stringify(res)}`);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type InitialOpts,
|
||||
type DefaultDeleteSessionResult,
|
||||
type SingularSessionData,
|
||||
type SessionCapabilities,
|
||||
} from '@appium/types';
|
||||
import B from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
@@ -24,7 +25,7 @@ import {calcSignature} from '../helpers/session';
|
||||
import {DELETE_SESSION_COMMAND, determineProtocol, errors} from '../protocol';
|
||||
import {processCapabilities, validateCaps} from './capabilities';
|
||||
import {DriverCore} from './core';
|
||||
import helpers from './helpers';
|
||||
import * as helpers from './helpers';
|
||||
|
||||
const EVENT_SESSION_INIT = 'newSessionRequested';
|
||||
const EVENT_SESSION_START = 'newSessionStarted';
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import {logger} from '@appium/support';
|
||||
import {EventEmitter} from 'node:events';
|
||||
import type {
|
||||
AppiumLogger,
|
||||
BidiModuleMap,
|
||||
BiDiResultData,
|
||||
StringRecord,
|
||||
} from '@appium/types';
|
||||
import {
|
||||
MAX_LOG_BODY_LENGTH,
|
||||
} from '../constants';
|
||||
import {errors} from '../protocol';
|
||||
import {BIDI_COMMANDS} from '../protocol/bidi-commands';
|
||||
import _ from 'lodash';
|
||||
import {generateDriverLogPrefix} from './helpers';
|
||||
|
||||
export class ExtensionCore {
|
||||
bidiEventSubs: Record<string, string[]>;
|
||||
bidiCommands: BidiModuleMap = BIDI_COMMANDS as BidiModuleMap;
|
||||
_logPrefix?: string;
|
||||
protected _log: AppiumLogger;
|
||||
// used to handle driver events
|
||||
readonly eventEmitter: NodeJS.EventEmitter;
|
||||
|
||||
|
||||
constructor(logPrefix?: string) {
|
||||
this._logPrefix = logPrefix;
|
||||
this.bidiEventSubs = {};
|
||||
this.eventEmitter = new EventEmitter();
|
||||
}
|
||||
|
||||
get log(): AppiumLogger {
|
||||
if (!this._log) {
|
||||
this.updateLogPrefix(this._logPrefix ?? generateDriverLogPrefix(this));
|
||||
}
|
||||
return this._log;
|
||||
}
|
||||
|
||||
updateLogPrefix(logPrefix: string) {
|
||||
this._log = logger.getLogger(logPrefix);
|
||||
}
|
||||
|
||||
updateBidiCommands(cmds: BidiModuleMap): void {
|
||||
const overlappingKeys = _.intersection(Object.keys(cmds), Object.keys(this.bidiCommands));
|
||||
if (overlappingKeys.length) {
|
||||
this.log.warn(`Overwriting existing bidi modules: ${JSON.stringify(overlappingKeys)}. This may not be intended!`);
|
||||
}
|
||||
this.bidiCommands = {
|
||||
...this.bidiCommands,
|
||||
...cmds,
|
||||
};
|
||||
}
|
||||
|
||||
doesBidiCommandExist(moduleName: string, methodName: string): boolean {
|
||||
try {
|
||||
this.ensureBidiCommandExists(moduleName, methodName);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ensureBidiCommandExists(moduleName: string, methodName: string): void {
|
||||
// if we don't get a valid format for bidi command name, reject
|
||||
if (!moduleName || !methodName) {
|
||||
throw new errors.UnknownCommandError(
|
||||
`Did not receive a valid BiDi module and method name ` +
|
||||
`of the form moduleName.methodName. Instead received ` +
|
||||
`'${moduleName}.${methodName}'`,
|
||||
);
|
||||
}
|
||||
|
||||
// if the command module or method isn't part of our spec, reject
|
||||
if (!this.bidiCommands[moduleName] || !this.bidiCommands[moduleName][methodName]) {
|
||||
throw new errors.UnknownCommandError();
|
||||
}
|
||||
|
||||
const {command} = this.bidiCommands[moduleName][methodName];
|
||||
// if the command method isn't part of our spec, also reject
|
||||
if (!command) {
|
||||
throw new errors.UnknownCommandError();
|
||||
}
|
||||
|
||||
// If the driver doesn't have this command, it must not be implemented
|
||||
if (!this[command]) {
|
||||
throw new errors.NotYetImplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
async executeBidiCommand(bidiCmd: string, bidiParams: StringRecord, next?: () => Promise<any>, driver?: ExtensionCore): Promise<BiDiResultData> {
|
||||
const handlerType = (next && driver) ? 'plugin' : 'driver';
|
||||
const [moduleName, methodName] = bidiCmd.split('.');
|
||||
this.ensureBidiCommandExists(moduleName, methodName);
|
||||
const {command, params} = this.bidiCommands[moduleName][methodName];
|
||||
|
||||
// TODO improve param parsing and error messages along the lines of what we have in the http
|
||||
// handlers
|
||||
const args: any[] = [];
|
||||
if (params?.required?.length) {
|
||||
for (const requiredParam of params.required) {
|
||||
if (_.isUndefined(bidiParams[requiredParam])) {
|
||||
throw new errors.InvalidArgumentError(
|
||||
`The ${requiredParam} parameter was required but you omitted it`,
|
||||
);
|
||||
}
|
||||
args.push(bidiParams[requiredParam]);
|
||||
}
|
||||
}
|
||||
if (params?.optional?.length) {
|
||||
for (const optionalParam of params.optional) {
|
||||
args.push(bidiParams[optionalParam]);
|
||||
}
|
||||
}
|
||||
const logParams = _.truncate(JSON.stringify(bidiParams), {length: MAX_LOG_BODY_LENGTH});
|
||||
this.log.debug(
|
||||
`Executing bidi command '${bidiCmd}' with params ${logParams} by passing to ${handlerType} ` +
|
||||
`method '${command}'`,
|
||||
);
|
||||
// call the handler with the signature appropriate to extension type (plugin or driver)
|
||||
const response = (next && driver) ? await this[command](next, driver, ...args) : await this[command](...args);
|
||||
const finalResponse = _.isUndefined(response) ? {} : response;
|
||||
this.log.debug(
|
||||
`Responding to bidi command '${bidiCmd}' with ` +
|
||||
`${_.truncate(JSON.stringify(finalResponse), {length: MAX_LOG_BODY_LENGTH})}`
|
||||
);
|
||||
return finalResponse;
|
||||
}
|
||||
}
|
||||
@@ -389,8 +389,8 @@ export function parseCapsArray(cap) {
|
||||
if (_.isArray(parsedCaps)) {
|
||||
return parsedCaps;
|
||||
}
|
||||
} catch (ign) {
|
||||
logger.warn(`Failed to parse capability as JSON array`);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to parse capability as JSON array: ${e.message}`);
|
||||
}
|
||||
if (_.isString(cap)) {
|
||||
return [cap];
|
||||
@@ -401,11 +401,12 @@ export function parseCapsArray(cap) {
|
||||
/**
|
||||
* Generate a string that uniquely describes driver instance
|
||||
*
|
||||
* @param {import('@appium/types').Core} obj driver instance
|
||||
* @param {object} obj driver instance
|
||||
* @param {string?} [sessionId=null] session identifier (if exists).
|
||||
* This parameter is deprecated and is not used.
|
||||
* @returns {string}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function generateDriverLogPrefix(obj, sessionId = null) {
|
||||
return `${obj.constructor.name}@${node.getObjectId(obj).substring(0, 4)}`;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ const startLogFormatter = morgan(
|
||||
reqBody = _.truncate(_.isString(req.body) ? req.body : JSON.stringify(req.body), {
|
||||
length: MAX_LOG_BODY_LENGTH,
|
||||
});
|
||||
} catch (ign) {}
|
||||
} catch {}
|
||||
}
|
||||
log.info(requestStartLoggingFormat(tokens, req, res), reqBody.grey);
|
||||
},
|
||||
|
||||
@@ -60,7 +60,7 @@ async function createServer (app, cliArgs) {
|
||||
}
|
||||
const [cert, key] = await B.all(certKey.map((p) => fs.readFile(p, 'utf8')));
|
||||
log.debug('Enabling TLS/SPDY on the server using the provided certificate');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
|
||||
return require('spdy').createServer({
|
||||
cert,
|
||||
key,
|
||||
@@ -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,
|
||||
@@ -151,6 +158,7 @@ function configureServer({
|
||||
|
||||
// set up static assets
|
||||
app.use(favicon(path.resolve(STATIC_DIR, 'favicon.ico')));
|
||||
// eslint-disable-next-line import/no-named-as-default-member
|
||||
app.use(express.static(STATIC_DIR));
|
||||
|
||||
// crash routes, for testing
|
||||
@@ -266,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;
|
||||
@@ -283,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}`);
|
||||
}
|
||||
@@ -301,7 +318,6 @@ function normalizeBasePath(basePath) {
|
||||
return basePath;
|
||||
}
|
||||
|
||||
export {server, configureServer, normalizeBasePath};
|
||||
|
||||
/**
|
||||
* Options for {@linkcode startServer}.
|
||||
@@ -310,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
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -343,6 +361,7 @@ export {server, configureServer, normalizeBasePath};
|
||||
* @property {MethodMap} [extraMethodMap]
|
||||
* @property {import('@appium/types').UpdateServerCallback[]} [serverUpdaters]
|
||||
* @property {number} [keepAliveTimeout]
|
||||
* @property {number} [requestTimeout]
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable require-await */
|
||||
|
||||
import _ from 'lodash';
|
||||
import B from 'bluebird';
|
||||
|
||||
@@ -41,7 +41,7 @@ async function removeWebSocketHandler(handlerPathname) {
|
||||
client.terminate();
|
||||
}
|
||||
return true;
|
||||
} catch (ign) {
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
delete this.webSocketsMapping[handlerPathname];
|
||||
|
||||
@@ -4,12 +4,13 @@ try {
|
||||
B.config({
|
||||
cancellation: true,
|
||||
});
|
||||
} catch (ign) {
|
||||
} catch {
|
||||
// sometimes during testing this somehow gets required twice and results in an error about
|
||||
// cancellation not being able to be enabled after promise has been configured
|
||||
}
|
||||
|
||||
// BaseDriver exports
|
||||
export {ExtensionCore} from './basedriver/extension-core';
|
||||
import {BaseDriver} from './basedriver/driver';
|
||||
export {DriverCore} from './basedriver/core';
|
||||
export {DeviceSettings} from './basedriver/device-settings';
|
||||
@@ -48,6 +49,11 @@ 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';
|
||||
|
||||
/**
|
||||
* @typedef {import('./express/server').ServerOpts} ServerOpts
|
||||
*/
|
||||
|
||||
@@ -15,6 +15,7 @@ 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';
|
||||
|
||||
const DEFAULT_LOG = logger.getLogger('WD Proxy');
|
||||
const DEFAULT_REQUEST_TIMEOUT = 240000;
|
||||
@@ -110,8 +111,24 @@ class JWProxy {
|
||||
this._activeRequests = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the given endpoint started with '/session' and
|
||||
* it could have session id after the path.
|
||||
* e.g.
|
||||
* - should return true
|
||||
* - /session/82a9b7da-faaf-4a1d-8ef3-5e4fb5812200
|
||||
* - /session/82a9b7da-faaf-4a1d-8ef3-5e4fb5812200/url
|
||||
* - should return false
|
||||
* - /session
|
||||
* - /sessions
|
||||
* - /session/
|
||||
* - /status
|
||||
* - /appium/sessions
|
||||
* @param {string} endpoint
|
||||
* @returns {boolean}
|
||||
*/
|
||||
endpointRequiresSessionId(endpoint) {
|
||||
return !_.includes(['/session', '/sessions', '/status'], endpoint);
|
||||
return !!(pathToRegexMatch('/session/:sessionId{/*command}')(endpoint));
|
||||
}
|
||||
|
||||
set downstreamProtocol(value) {
|
||||
@@ -128,7 +145,7 @@ class JWProxy {
|
||||
url = '/';
|
||||
}
|
||||
const proxyBase = `${this.scheme}://${this.server}:${this.port}${this.base}`;
|
||||
const endpointRe = '(/(session|status))';
|
||||
const endpointRe = '(/(session|status|appium))';
|
||||
let remainingUrl = '';
|
||||
if (/^http/.test(url)) {
|
||||
const first = new RegExp(`(https?://.+)${endpointRe}`).exec(url);
|
||||
@@ -142,7 +159,7 @@ class JWProxy {
|
||||
throw new Error(`Did not know what to do with url '${url}'`);
|
||||
}
|
||||
|
||||
const stripPrefixRe = new RegExp('^.*?(/(session|status).*)$');
|
||||
const stripPrefixRe = new RegExp('^.*?(/(session|status|appium).*)$');
|
||||
if (stripPrefixRe.test(remainingUrl)) {
|
||||
remainingUrl = /** @type {RegExpExecArray} */ (stripPrefixRe.exec(remainingUrl))[1];
|
||||
}
|
||||
@@ -207,7 +224,7 @@ class JWProxy {
|
||||
if (typeof body !== 'object') {
|
||||
try {
|
||||
reqOpts.data = JSON.parse(body);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
throw new Error(`Cannot interpret the request body as valid JSON: ${truncateBody(body)}`);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -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',
|
||||
@@ -13,6 +13,10 @@ const BIDI_COMMANDS = /** @type {const} */ ({
|
||||
command: 'bidiUnsubscribe',
|
||||
params: SUBSCRIPTION_REQUEST_PARAMS,
|
||||
},
|
||||
status: {
|
||||
command: 'bidiStatus',
|
||||
params: {},
|
||||
}
|
||||
},
|
||||
browsingContext: {
|
||||
navigate: {
|
||||
@@ -27,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};
|
||||
|
||||
@@ -60,16 +60,14 @@ export class ProtocolError extends BaseError {
|
||||
* Get the Bidi protocol version of an error
|
||||
* @param {string|number} id - the id used in the request for which this error forms the response
|
||||
* @see https://w3c.github.io/webdriver-bidi/#protocol-definition
|
||||
* @returns The object conforming to the shape of a BiDi error response
|
||||
* @returns {import('@appium/types').ErrorBiDiCommandResponse} The object conforming to the shape of a BiDi error response
|
||||
*/
|
||||
bidiErrObject(id) {
|
||||
// if we don't have an id, the client didn't send one, so we have nothing to send back.
|
||||
// send back an empty string rather than making something up
|
||||
if (_.isNil(id)) {
|
||||
id = '';
|
||||
}
|
||||
// send back zero rather than making something up
|
||||
const intId = /** @type {number} */ (_.isInteger(id) ? id : (parseInt(`${id}`, 10) || 0));
|
||||
return {
|
||||
id,
|
||||
id: intId,
|
||||
type: 'error',
|
||||
error: this.error,
|
||||
stacktrace: this.stacktrace,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -22,6 +22,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 +35,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 +44,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 +63,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 +73,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 +89,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 +104,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 +124,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 +165,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 +185,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 +196,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 +207,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 +222,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 +243,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 +254,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 +619,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 +637,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 +661,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 +701,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 +807,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 +896,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 +903,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 +922,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
|
||||
@@ -963,4 +978,4 @@ export function routeToCommandName(endpoint, method, basePath = DEFAULT_BASE_PAT
|
||||
}
|
||||
|
||||
// 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.13.1",
|
||||
"version": "9.16.0",
|
||||
"description": "Base driver class for Appium drivers",
|
||||
"keywords": [
|
||||
"automation",
|
||||
@@ -44,21 +44,16 @@
|
||||
"test:types": "tsd"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appium/support": "^6.0.0",
|
||||
"@appium/types": "^0.22.3",
|
||||
"@appium/support": "^6.0.4",
|
||||
"@appium/types": "^0.25.0",
|
||||
"@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.13",
|
||||
"@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",
|
||||
"bluebird": "3.7.2",
|
||||
"body-parser": "1.20.3",
|
||||
"express": "4.21.1",
|
||||
"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.30.0",
|
||||
"type-fest": "4.35.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
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {server} from '../../../lib';
|
||||
import axios from 'axios';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import B from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
@@ -85,7 +86,11 @@ describe('server', function () {
|
||||
}).should.be.rejectedWith(/EADDRINUSE/);
|
||||
});
|
||||
it('should not wait for the server close connections before finishing closing', async function () {
|
||||
let bodyPromise = axios.get(`http://${TEST_HOST}:${port}/pause`).catch(() => {});
|
||||
const bodyPromise = (async () => {
|
||||
try {
|
||||
return await axios.get(`http://${TEST_HOST}:${port}/pause`);
|
||||
} catch {}
|
||||
})();
|
||||
|
||||
// relinquish control so that we don't close before the request is received
|
||||
await B.delay(100);
|
||||
@@ -188,11 +193,11 @@ describe('server plugins', function () {
|
||||
afterEach(async function () {
|
||||
try {
|
||||
await hwServer.close();
|
||||
} catch (ign) {}
|
||||
} catch {}
|
||||
});
|
||||
|
||||
function updaterWithGetRoute(route, reply) {
|
||||
// eslint-disable-next-line require-await
|
||||
|
||||
return async (app, httpServer) => {
|
||||
app.get(`/${route}`, (req, res) => {
|
||||
res.header['content-type'] = 'text/html';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable require-await */
|
||||
|
||||
import {errors, BaseDriver, determineProtocol} from '../../../lib';
|
||||
import {PROTOCOLS} from '../../../lib/constants';
|
||||
import {util} from '@appium/support';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line import/no-named-as-default
|
||||
import Express from 'express';
|
||||
import bodyParser from 'body-parser';
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {server, routeConfiguringFunction, errors, JWProxy, BaseDriver} from '../../../lib';
|
||||
import {FakeDriver} from './fake-driver';
|
||||
import axios from 'axios';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {StatusCodes as HTTPStatusCodes} from 'http-status-codes';
|
||||
import {createProxyServer} from './helpers';
|
||||
@@ -917,7 +918,7 @@ describe('Protocol', function () {
|
||||
});
|
||||
|
||||
it('should pass on any errors in proxying', async function () {
|
||||
// eslint-disable-next-line require-await
|
||||
|
||||
driver.proxyReqRes = async function () {
|
||||
throw new Error('foo');
|
||||
};
|
||||
@@ -938,7 +939,7 @@ describe('Protocol', function () {
|
||||
});
|
||||
|
||||
it('should able to throw ProxyRequestError in proxying', async function () {
|
||||
// eslint-disable-next-line require-await
|
||||
|
||||
driver.proxyReqRes = async function () {
|
||||
let jsonwp = {
|
||||
status: 35,
|
||||
@@ -961,7 +962,7 @@ describe('Protocol', function () {
|
||||
});
|
||||
|
||||
it('should let the proxy handle req/res', async function () {
|
||||
// eslint-disable-next-line require-await
|
||||
|
||||
driver.proxyReqRes = async function (req, res) {
|
||||
res.status(200).json({custom: 'data'});
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import B from 'bluebird';
|
||||
import {BaseDriver, errors} from '../../../lib/index';
|
||||
import {validator} from '../../../lib/basedriver/desired-caps';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
|
||||
// TODO: we need module-level mocks for the logger
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// @ts-check
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import _ from 'lodash';
|
||||
import BaseDriver from '../../../../lib';
|
||||
import {BaseDriver} from '../../../../lib';
|
||||
|
||||
const FIRST_LOGS = ['first', 'logs'];
|
||||
const SECOND_LOGS = ['second', 'logs'];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import BaseDriver from '../../../lib';
|
||||
import {BaseDriver} from '../../../lib';
|
||||
import {driverUnitTestSuite} from '@appium/driver-test-support';
|
||||
|
||||
driverUnitTestSuite(BaseDriver, {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-check
|
||||
|
||||
import BaseDriver from '../../../lib';
|
||||
import {BaseDriver} from '../../../lib';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
|
||||
describe('timeout', function () {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import {server, routeConfiguringFunction} from '../../../lib';
|
||||
import {configureServer, normalizeBasePath} from '../../../lib/express/server';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
import {getTestPort} from '@appium/driver-test-support';
|
||||
|
||||
@@ -31,6 +32,8 @@ describe('server configuration', function () {
|
||||
const chaiAsPromised = await import('chai-as-promised');
|
||||
chai.use(chaiAsPromised.default);
|
||||
should = chai.should();
|
||||
|
||||
port = await getTestPort(true);
|
||||
});
|
||||
|
||||
function fakeApp() {
|
||||
@@ -50,10 +53,6 @@ describe('server configuration', function () {
|
||||
return app;
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
port = await getTestPort(true);
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
sandbox = createSandbox();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// transpile:mocha
|
||||
|
||||
import {welcome} from '../../../lib/express/static';
|
||||
// eslint-disable-next-line import/named
|
||||
import {createSandbox} from 'sinon';
|
||||
|
||||
describe('welcome', function () {
|
||||
|
||||
@@ -20,7 +20,7 @@ function resFixture(url, method) {
|
||||
throw new Error("Can't handle url " + url);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-await
|
||||
|
||||
async function request(opts) {
|
||||
const {url, method, json} = opts;
|
||||
if (/badurl$/.test(url)) {
|
||||
|
||||
@@ -17,7 +17,7 @@ function buildReqRes(url, method, body) {
|
||||
res.send = (body) => {
|
||||
try {
|
||||
body = JSON.parse(body);
|
||||
} catch (e) {}
|
||||
} catch {}
|
||||
res.sentBody = body;
|
||||
};
|
||||
return [req, res];
|
||||
@@ -32,6 +32,7 @@ describe('proxy', function () {
|
||||
const chaiAsPromised = await import('chai-as-promised');
|
||||
chai.use(chaiAsPromised.default);
|
||||
should = chai.should();
|
||||
port = await getTestPort();
|
||||
});
|
||||
|
||||
function mockProxy(opts = {}) {
|
||||
@@ -44,10 +45,6 @@ describe('proxy', function () {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
port = await getTestPort();
|
||||
});
|
||||
|
||||
it('should override default params', function () {
|
||||
let j = mockProxy({server: '127.0.0.2', port});
|
||||
j.server.should.equal('127.0.0.2');
|
||||
@@ -110,6 +107,12 @@ describe('proxy', function () {
|
||||
let j = mockProxy();
|
||||
let newUrl = j.getUrlForProxy('/status');
|
||||
|
||||
should.exist(newUrl);
|
||||
});
|
||||
it('should not throw an error if url does not require a session id with appium vendor prefix and its null', function () {
|
||||
let j = mockProxy();
|
||||
let newUrl = j.getUrlForProxy('/appium/something');
|
||||
|
||||
should.exist(newUrl);
|
||||
});
|
||||
});
|
||||
@@ -212,4 +215,30 @@ describe('proxy', function () {
|
||||
res.sentBody.should.eql({value: {message: 'chrome not reachable'}});
|
||||
});
|
||||
});
|
||||
describe('endpointRequiresSessionId', function () {
|
||||
const j = mockProxy({sessionId: '123'});
|
||||
|
||||
[
|
||||
'/session/82a9b7da-faaf-4a1d-8ef3-5e4fb5812200',
|
||||
'/session/82a9b7da-faaf-4a1d-8ef3-5e4fb5812200/',
|
||||
'/session/82a9b7da-faaf-4a1d-8ef3-5e4fb5812200/url',
|
||||
'/session/82a9b7da-faaf-4a1d-8ef3-5e4fb5812200/element/3d001db2-7987-42a7-975d-8d5d5304083f',
|
||||
].forEach(function (endpoint) {
|
||||
it(`should be true with ${endpoint}`, function () {
|
||||
j.endpointRequiresSessionId(endpoint).should.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
[
|
||||
'/session',
|
||||
'/session/',
|
||||
'/sessions',
|
||||
'/appium/sessions',
|
||||
'/status',
|
||||
].forEach(function (endpoint) {
|
||||
it(`should be false with ${endpoint}`, function () {
|
||||
j.endpointRequiresSessionId(endpoint).should.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// transpile:mocha
|
||||
|
||||
import {_} from 'lodash';
|
||||
import _ from 'lodash';
|
||||
import {METHOD_MAP, routeToCommandName} from '../../../lib/protocol';
|
||||
import crypto from 'crypto';
|
||||
|
||||
@@ -43,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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,11 +51,6 @@ describe('Protocol', function () {
|
||||
cmdName.should.equal('timeouts');
|
||||
});
|
||||
|
||||
it('should properly lookup correct command name for endpoint with session', function () {
|
||||
const cmdName = routeToCommandName('/timeouts/implicit_wait', 'POST');
|
||||
cmdName.should.equal('implicitWait');
|
||||
});
|
||||
|
||||
it('should properly lookup correct command name for endpoint without session', function () {
|
||||
const cmdName = routeToCommandName('/status', 'GET');
|
||||
cmdName.should.equal('getStatus');
|
||||
|
||||
@@ -3,6 +3,40 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **appium,base-driver,base-plugin:** allow plugins to define custom bidi commands and emit bidi events ([#20876](https://github.com/appium/appium/issues/20876)) ([8df1c21](https://github.com/appium/appium/commit/8df1c217a15d30300c04b9f59cdbdffa70325828))
|
||||
|
||||
|
||||
|
||||
## [2.2.52](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.51...@appium/base-plugin@2.2.52) (2025-01-06)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.2.51](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.50...@appium/base-plugin@2.2.51) (2025-01-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Reduce linter warnings ([#20860](https://github.com/appium/appium/issues/20860)) ([65658cc](https://github.com/appium/appium/commit/65658ccbdde9144c45cb5aad6a9089a5d6f3a0a3))
|
||||
|
||||
|
||||
|
||||
## [2.2.50](https://github.com/appium/appium/compare/@appium/base-plugin@2.2.49...@appium/base-plugin@2.2.50) (2024-12-05)
|
||||
|
||||
**Note:** Version bump only for package @appium/base-plugin
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import {logger} from '@appium/support';
|
||||
import {validateExecuteMethodParams} from '@appium/base-driver';
|
||||
import {ExtensionCore, generateDriverLogPrefix, validateExecuteMethodParams} from '@appium/base-driver';
|
||||
|
||||
/**
|
||||
* @implements {Plugin}
|
||||
*/
|
||||
class BasePlugin {
|
||||
class BasePlugin extends ExtensionCore {
|
||||
|
||||
/**
|
||||
* @deprecated Use this.log instead of this.logger
|
||||
* @type {import('@appium/types').AppiumLogger}
|
||||
*/
|
||||
logger;
|
||||
|
||||
/**
|
||||
* Subclasses should use type `import('@appium/types').MethodMap<SubclassName>`.
|
||||
*
|
||||
@@ -43,11 +49,16 @@ class BasePlugin {
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Record<string,unknown>} [cliArgs]
|
||||
* @param {string|null} driverId
|
||||
*/
|
||||
constructor(name, cliArgs = {}) {
|
||||
constructor(name, cliArgs = {}, driverId = null) {
|
||||
super();
|
||||
if (driverId) {
|
||||
this.updateLogPrefix(`${generateDriverLogPrefix(this)} <${driverId}>`);
|
||||
}
|
||||
this.name = name;
|
||||
this.cliArgs = cliArgs;
|
||||
this.logger = logger.getLogger(`Plugin [${name}]`);
|
||||
this.logger = this.log;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@appium/base-plugin",
|
||||
"version": "2.2.50",
|
||||
"version": "2.3.1",
|
||||
"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.13.1",
|
||||
"@appium/support": "^6.0.0"
|
||||
"@appium/base-driver": "^9.16.0",
|
||||
"@appium/support": "^6.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import BasePlugin from '../../lib/plugin';
|
||||
import {BasePlugin} from '../../lib/plugin';
|
||||
|
||||
describe('base plugin', function () {
|
||||
let should;
|
||||
|
||||
@@ -3,6 +3,55 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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.0.28](https://github.com/appium/appium/compare/@appium/docutils@1.0.27...@appium/docutils@1.0.28) (2025-01-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **support:** update dependency teen_process to v2.2.3 ([#20882](https://github.com/appium/appium/issues/20882)) ([488e5de](https://github.com/appium/appium/commit/488e5dea82d3c525acf483952570e32f319f3722))
|
||||
|
||||
|
||||
|
||||
## [1.0.27](https://github.com/appium/appium/compare/@appium/docutils@1.0.26...@appium/docutils@1.0.27) (2025-01-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **docutils:** update dependency consola to v3.3.0 ([#20851](https://github.com/appium/appium/issues/20851)) ([aa88083](https://github.com/appium/appium/commit/aa880837da3bdbbc531e9d4c13b8baa76573cb7e))
|
||||
* **docutils:** update dependency consola to v3.3.1 ([#20855](https://github.com/appium/appium/issues/20855)) ([861d060](https://github.com/appium/appium/commit/861d060713dade7b6c55b2b0d49e97c6e9e66103))
|
||||
* **docutils:** update dependency consola to v3.3.3 ([#20858](https://github.com/appium/appium/issues/20858)) ([4b3a8c2](https://github.com/appium/appium/commit/4b3a8c2197a587ec5a2606b662e4e7667a7852cb))
|
||||
* **docutils:** update dependency yaml to v2.7.0 ([#20870](https://github.com/appium/appium/issues/20870)) ([389a3e8](https://github.com/appium/appium/commit/389a3e83ac0069504338f7182a4e938cd391de7e))
|
||||
* Reduce linter warnings ([#20860](https://github.com/appium/appium/issues/20860)) ([65658cc](https://github.com/appium/appium/commit/65658ccbdde9144c45cb5aad6a9089a5d6f3a0a3))
|
||||
* **types:** update dependency type-fest to v4 ([#20838](https://github.com/appium/appium/issues/20838)) ([a5897dd](https://github.com/appium/appium/commit/a5897dd25a277a42b0c650a52274ba2c891ac3b0))
|
||||
* **types:** update dependency type-fest to v4 ([#20843](https://github.com/appium/appium/issues/20843)) ([7abecad](https://github.com/appium/appium/commit/7abecaddd3ed64c7be321650b2a17990e74a7222))
|
||||
* **types:** update dependency type-fest to v4.31.0 ([#20857](https://github.com/appium/appium/issues/20857)) ([24abb38](https://github.com/appium/appium/commit/24abb385e54f57457c4fb3f2b654cb63645e7ccd))
|
||||
|
||||
|
||||
|
||||
## [1.0.26](https://github.com/appium/appium/compare/@appium/docutils@1.0.25...@appium/docutils@1.0.26) (2024-12-05)
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const {getLogger} = require('../build/lib/logger');
|
||||
|
||||
const log = getLogger('cli');
|
||||
|
||||
// eslint-disable-next-line promise/prefer-await-to-then, promise/prefer-await-to-callbacks
|
||||
// eslint-disable-next-line promise/prefer-await-to-callbacks
|
||||
main().catch((err) => {
|
||||
log.error('Caught otherwise-unhandled rejection (this is probably a bug):', err);
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user