Compare commits

...
Author SHA1 Message Date
Jonathan Lipps a7ce83e169 chore: publish
- appium@2.15.0
 - @appium/base-driver@9.15.0
 - @appium/base-plugin@2.3.0
 - @appium/docutils@1.0.29
 - @appium/driver-test-support@0.7.4
 - @appium/eslint-config-appium-ts@1.0.2
 - @appium/fake-driver@5.7.0
 - @appium/fake-plugin@3.2.0
 - @appium/images-plugin@3.0.27
 - @appium/plugin-test-support@0.3.49
 - @appium/support@6.0.3
 - @appium/test-support@3.1.3
 - @appium/types@0.24.0
2025-01-08 13:57:17 -08:00
Jonathan Lipps 8df1c217a1 feat(appium,base-driver,base-plugin): allow plugins to define custom bidi commands and emit bidi events (#20876)
* feat: allow drivers to define their own bidi commands

* test(appium): refactor plugin tests server startup

* feat: allow plugins to define their own bidi commands and events

* fix(fake-driver,fake-plugin): ensure custom bidi commands events have vendor prefixes

* feat(appium): link plugin and driver objects in log lines when possible

* fix(appium,base-driver): ensure new methods for plugins aren't called without existence checks
2025-01-08 13:45:36 -08:00
Edgars Eglītis aad5709913 fix(eslint-config-appium-ts): add Prettier config, update typescript-eslint imports (#20887) 2025-01-07 20:25:50 +02:00
Mykola Mokhnach 17ecc21747 chore(eslint-config-appium-ts): Use .gitingore from the current workdir rather than same shared config dir (#20888) 2025-01-07 16:08:17 +01:00
Jonathan Lipps 3467f32c8e chore: publish
- appium@2.14.1
 - @appium/base-driver@9.14.1
 - @appium/base-plugin@2.2.52
 - @appium/docutils@1.0.28
 - @appium/driver-test-support@0.7.3
 - @appium/execute-driver-plugin@4.0.1
 - @appium/fake-plugin@3.1.56
 - @appium/images-plugin@3.0.26
 - @appium/plugin-test-support@0.3.48
 - @appium/support@6.0.2
 - @appium/test-support@3.1.2
2025-01-06 14:50:32 -08:00
renovate[bot] 6f27d36cf5 fix(support): update dependency uuid to v11.0.4 (#20883)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-06 12:24:23 +00:00
renovate[bot] 488e5dea82 fix(support): update dependency teen_process to v2.2.3 (#20882)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-06 11:33:01 +00:00
Mykola Mokhnach 13d8b03cc1 fix(appium): Do not update BiDi commands for drivers which don't support it (#20879) 2025-01-05 15:04:26 +01:00
renovate[bot] fe209ed660 fix(driver-test-support): update dependency @types/lodash to v4.17.14 (#20877)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-04 09:48:30 +00:00
renovate[bot] 93e4288253 fix(execute-driver-plugin): update dependency webdriverio to v9.5.1 (#20875)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-03 15:45:30 +00:00
renovate[bot] fb839be6ea chore(deps): update dependency @types/node to v22.10.5 (#20874)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-03 12:40:58 +00:00
Jonathan Lipps 6742808345 chore: publish
- appium@2.14.0
 - @appium/base-driver@9.14.0
 - @appium/base-plugin@2.2.51
 - @appium/docutils@1.0.27
 - @appium/driver-test-support@0.7.2
 - @appium/eslint-config-appium-ts@1.0.1
 - @appium/execute-driver-plugin@4.0.0
 - @appium/fake-driver@5.6.0
 - @appium/fake-plugin@3.1.55
 - @appium/images-plugin@3.0.25
 - @appium/opencv@3.0.7
 - @appium/plugin-test-support@0.3.47
 - @appium/schema@0.7.1
 - @appium/support@6.0.1
 - @appium/test-support@3.1.1
 - @appium/types@0.23.0
 - @appium/universal-xml-plugin@1.0.27
2025-01-02 10:59:35 -08:00
renovate[bot] bb4ba0e52b chore(deps): update dependency @types/node to v22.10.3 (#20872)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-01 09:10:42 +00:00
renovate[bot] 990a0667e3 fix(execute-driver-plugin): update dependency webdriverio to v9.5.0 (#20871)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-31 19:27:42 +00:00
renovate[bot] 389a3e83ac fix(docutils): update dependency yaml to v2.7.0 (#20870)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-31 16:54:07 +00:00
renovate[bot] a034880a03 chore(deps): update eslint-related packages to v8.19.0 (#20869)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-31 13:48:03 +00:00
Mykola Mokhnach 588381ecf4 test(appium): Delete obsolete todo 2024-12-31 09:46:23 +01:00
Mykola Mokhnach 3fc57a15b5 test(appium): Update Bidi over ssl test (#20868) 2024-12-31 09:44:25 +01:00
Mykola Mokhnach bbfc04026f feat(appium): Update entry point resolution logic for ESM extensions (#20866) 2024-12-30 10:44:34 +01:00
Kazuaki Matsuo c5443e9fc9 feat(fake-driver): add getWindowHandle and getWindowHandles to reduce wdio errors in CI (#20865) 2024-12-28 09:28:10 -08:00
renovate[bot] 31c6759061 chore(workflows): update peter-evans/create-pull-request action to v7.0.6 (#20862)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-28 09:01:19 +00:00
renovate[bot] edd1c05d1e chore(deps): update dependency @eslint/js to v9.17.0 (#20863)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-28 09:59:34 +01:00
Edgars Eglītis 61b6689897 chore: adjust eslint-config-appium-ts related files (#20861) 2024-12-28 09:30:40 +01:00
Mykola Mokhnach 65658ccbdd fix: Reduce linter warnings (#20860) 2024-12-28 09:30:05 +01:00
Mykola Mokhnach 25ea9f73bc chore: Migrate to flat ESLint config (v9) (#20856) 2024-12-27 20:42:36 +01:00
renovate[bot] 4b3a8c2197 fix(docutils): update dependency consola to v3.3.3 (#20858)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-27 09:53:16 +00:00
renovate[bot] 24abb385e5 fix(types): update dependency type-fest to v4.31.0 (#20857)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-26 10:46:20 +00:00
renovate[bot] 861d060713 fix(docutils): update dependency consola to v3.3.1 (#20855)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 13:32:06 +00:00
Mykola Mokhnach c57f81d134 chore(appium): Tune BiDi logs (#20854) 2024-12-22 09:08:34 +01:00
Mykola Mokhnach 51acf54fe5 chore(appium): Move mixin BiDi-related methods into a separate module (#20852) 2024-12-21 11:02:36 +01:00
renovate[bot] aa880837da fix(docutils): update dependency consola to v3.3.0 (#20851)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-20 12:07:58 +00:00
Mykola Mokhnach af7aba492d chore(appium): Do not log all BiDi events (#20849) 2024-12-20 08:38:20 +01:00
Kazuaki Matsuo 25b8483294 docs(appium): add webSocketUrl in the docs (#20840)
* docs: add webSocketUrl in the docs
2024-12-19 23:02:52 -05:00
renovate[bot] 4474412a60 fix(execute-driver-plugin): update dependency webdriverio to v9.4.5 (#20847)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-19 12:20:12 +00:00
renovate[bot] 7c740f9b3a chore(js): update dependency webdriverio to v9 (#20832) 2024-12-18 22:00:22 +01:00
Mykola Mokhnach c765bc31e0 fix(base-driver): Return an empty object if the corresponding API response is undefined (#20845) 2024-12-18 20:28:19 +01:00
renovate[bot]andMykola Mokhnach 7abecaddd3 fix(types): update dependency type-fest to v4 (#20843)
* fix(types): update dependency type-fest to v4

* cast to string

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Mykola Mokhnach <mokhnach@gmail.com>
2024-12-18 08:24:57 +01:00
renovate[bot] 083f7e2328 chore(docutils): update dependency mkdocs-material to v9.5.49 (#20842)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-17 14:34:28 +00:00
renovate[bot] aaef1d6962 fix(universal-xml-plugin): update dependency fast-xml-parser to v4.5.1 (#20841)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-15 13:03:08 +00:00
Mykola Mokhnach 64e768efb7 feat(appium): Add session.status BiDi command (#20839) 2024-12-14 19:41:12 +01:00
renovate[bot] a5897dd25a fix(types): update dependency type-fest to v4 (#20838)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-13 16:36:24 +01:00
renovate[bot] 66e3580731 chore(deps): update dependency @types/node to v22.10.2 (#20833)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-11 10:59:21 +00:00
Mykola Mokhnach 16e9f94903 feat(execute-driver-plugin): Bump webdriverio to v9 (#20824)
BREAKING CHANGE: webdriverio major version bumped to 9
2024-12-10 08:35:41 +01:00
Jonathan Lipps a917ec6ced feat(appium): allow drivers to define their own bidi commands (#20828)
* feat: allow drivers to define their own bidi commands

* docs(appium): add docs about adding custom bidi commands in drivers
2024-12-10 12:02:23 +09:00
renovate[bot] 342d6641ee chore(docutils): update dependency mkdocs-material to v9.5.48 (#20829)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 10:30:33 +00:00
Edgars Eglītis 04e2d1154b fix(appium): update CLI help messages (#20825)
* fix(appium): adjust some CLI output messages

* fix(appium): adjust more CLI output messages

* fix(appium): add reset command to CLI & update tests

* chore: revert name change for driver/plugin name argument
2024-12-06 20:14:24 +02:00
renovate[bot] c032352fad fix(base-driver): update dependency express to v4.21.2 (#20823)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 10:01:24 +00:00
Mykola Mokhnach e3774ef0c0 ci(appium): Export mkdocs config to Crowdin as a plain text file (#20822) 2024-12-05 22:40:52 +01:00
183 changed files with 5699 additions and 5688 deletions
-9
View File
@@ -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/**
-62
View File
@@ -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
View File
@@ -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
-5
View File
@@ -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:
+3
View File
@@ -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
View File
@@ -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))
+24
View File
@@ -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/**',
],
},
];
+3279 -4156
View File
File diff suppressed because it is too large Load Diff
+7 -8
View File
@@ -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,12 +85,13 @@
},
"devDependencies": {
"@colors/colors": "1.6.0",
"@eslint/js": "9.17.0",
"@tsconfig/node14": "14.1.2",
"@types/chai": "5.0.1",
"@types/chai-as-promised": "8.0.1",
"@types/diff": "6.0.0",
"@types/mocha": "10.0.10",
"@types/node": "22.10.1",
"@types/node": "22.10.5",
"@types/semver": "7.5.8",
"@types/sinon": "17.0.3",
"@types/sinon-chai": "4.0.0",
@@ -98,20 +99,17 @@
"@types/teen_process": "2.0.4",
"@types/ws": "8.5.13",
"@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-as-promised": "8.0.1",
"conventional-changelog-conventionalcommits": "7.0.2",
"cpy-cli": "5.0.0",
"eslint": "8.57.1",
"eslint": "9.17.0",
"eslint-config-prettier": "9.1.0",
"eslint-find-rules": "4.2.0",
"eslint-import-resolver-typescript": "3.7.0",
"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",
@@ -131,8 +129,9 @@
"ts-node": "10.9.2",
"tsd": "0.31.2",
"typescript": "5.7.2",
"typescript-eslint": "8.19.0",
"validate.js": "0.13.1",
"webdriverio": "8.40.6",
"webdriverio": "9.5.1",
"ws": "8.18.0",
"yaml-js": "0.3.1"
},
+43
View File
@@ -3,6 +3,49 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
@@ -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
+11 -1
View File
@@ -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
+42 -398
View File
@@ -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,21 @@ import {
CREATE_SESSION_COMMAND,
DELETE_SESSION_COMMAND,
GET_STATUS_COMMAND,
MAX_LOG_BODY_LENGTH,
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 bidiHelpers from './bidi';
const desiredCapabilityConstraints = /** @type {const} */ ({
automationName: {
@@ -151,17 +141,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 +157,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
@@ -265,347 +243,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 +332,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}`));
@@ -776,14 +420,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 = bidiHelpers.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 ` +
@@ -975,12 +618,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 +655,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 +751,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 +828,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 +841,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,6 +934,10 @@ class AppiumDriver extends DriverCore {
const dstSession = this.sessions[sessionId];
return dstSession && dstSession.canProxy(sessionId);
}
onBidiConnection = bidiHelpers.onBidiConnection;
onBidiMessage = bidiHelpers.onBidiMessage;
onBidiServerError = bidiHelpers.onBidiServerError;
}
// help decide which commands should be proxied to sub-drivers and which
@@ -1284,25 +946,6 @@ function isAppiumDriverCommand(cmd) {
return !isSessionCommand(cmd) || cmd === DELETE_SESSION_COMMAND;
}
/**
* 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}
*/
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();
}
/**
* Thrown when Appium tried to proxy a command using a driver's `proxyCommand` method but the
* method did not exist
@@ -1338,6 +981,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
*/
+470
View File
@@ -0,0 +1,470 @@
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}`);
}
/**
* 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 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));
}
}
+22 -32
View File
@@ -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}.`,
},
],
]);
+4 -4
View File
@@ -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,
+33 -17
View File
@@ -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'
},
];
+3 -1
View File
@@ -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);
}
}
/**
+7 -7
View File
@@ -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.
*
+2 -2
View File
@@ -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}`,
+2 -2
View File
@@ -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);
}
+2 -2
View File
@@ -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;
}
};
+10 -10
View File
@@ -1,6 +1,6 @@
{
"name": "appium",
"version": "2.13.1",
"version": "2.15.0",
"description": "Automation for Apps.",
"keywords": [
"automation",
@@ -60,13 +60,13 @@
"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.15.0",
"@appium/base-plugin": "^2.3.0",
"@appium/docutils": "^1.0.29",
"@appium/logger": "^1.6.1",
"@appium/schema": "^0.7.0",
"@appium/support": "^6.0.0",
"@appium/types": "^0.22.3",
"@appium/schema": "^0.7.1",
"@appium/support": "^6.0.3",
"@appium/types": "^0.24.0",
"@sidvind/better-ajv-errors": "3.0.1",
"@types/argparse": "2.0.17",
"@types/bluebird": "3.5.42",
@@ -89,12 +89,12 @@
"resolve-from": "5.0.0",
"semver": "7.6.3",
"source-map-support": "0.5.21",
"teen_process": "2.2.2",
"type-fest": "4.30.0",
"teen_process": "2.2.3",
"type-fest": "4.31.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.5.1"
}
}
@@ -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,
+1 -1
View File
@@ -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';
+29 -53
View File
@@ -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();
@@ -251,8 +253,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 +264,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 +510,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 +546,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 +556,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 +606,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 +629,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 +643,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();
}
+114 -26
View File
@@ -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);
});
});
});
});
/**
@@ -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
View File
@@ -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>} */
+4 -1
View File
@@ -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
View File
@@ -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';
+1
View File
@@ -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';
+38
View File
@@ -3,6 +3,44 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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);
@@ -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) {
+14 -83
View File
@@ -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;
}
}
@@ -24,7 +24,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';
@@ -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);
},
+2 -1
View File
@@ -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,
@@ -151,6 +151,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
@@ -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 -1
View File
@@ -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,8 @@ export {
// Web socket helpers
export {DEFAULT_WS_PATHNAME_PREFIX} from './express/websocket';
export {generateDriverLogPrefix} from './basedriver/helpers';
/**
* @typedef {import('./express/server').ServerOpts} ServerOpts
*/
@@ -207,7 +207,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 {
@@ -13,6 +13,10 @@ const BIDI_COMMANDS = /** @type {const} */ ({
command: 'bidiUnsubscribe',
params: SUBSCRIPTION_REQUEST_PARAMS,
},
status: {
command: 'bidiStatus',
params: {},
}
},
browsingContext: {
navigate: {
+4 -6
View File
@@ -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,
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/base-driver",
"version": "9.13.1",
"version": "9.15.0",
"description": "Base driver class for Appium drivers",
"keywords": [
"automation",
@@ -44,13 +44,13 @@
"test:types": "tsd"
},
"dependencies": {
"@appium/support": "^6.0.0",
"@appium/types": "^0.22.3",
"@appium/support": "^6.0.3",
"@appium/types": "^0.24.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/lodash": "4.17.14",
"@types/method-override": "3.0.0",
"@types/serve-favicon": "2.5.7",
"async-lock": "1.4.1",
@@ -58,7 +58,7 @@
"axios": "1.7.9",
"bluebird": "3.7.2",
"body-parser": "1.20.3",
"express": "4.21.1",
"express": "4.21.2",
"http-status-codes": "2.3.0",
"lodash": "4.17.21",
"lru-cache": "10.4.3",
@@ -67,7 +67,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.31.0",
"validate.js": "0.13.1"
},
"optionalDependencies": {
@@ -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');
@@ -1,6 +1,4 @@
// transpile:mocha
import {_} from 'lodash';
import _ from 'lodash';
import {METHOD_MAP, routeToCommandName} from '../../../lib/protocol';
import crypto from 'crypto';
@@ -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');
+26
View File
@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.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
+16 -5
View File
@@ -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;
}
/**
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/base-plugin",
"version": "2.2.50",
"version": "2.3.0",
"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.15.0",
"@appium/support": "^6.0.3"
},
"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;
+33
View File
@@ -3,6 +3,39 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
+1 -1
View File
@@ -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);
});
+2 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env node
/* eslint-disable no-console */
/**
* Main CLI entry point for `@appium/docutils`
@@ -10,6 +10,7 @@ import {getLogger} from '../logger';
import {fs} from '@appium/support';
import _ from 'lodash';
// eslint-disable-next-line import/named
import {sync as readPkg} from 'read-pkg';
import {hideBin} from 'yargs/helpers';
import yargs from 'yargs/yargs';
+1 -1
View File
@@ -2,7 +2,7 @@
* Constants used across various modules in this package
* @module
*/
// eslint-disable-next-line @typescript-eslint/no-var-requires -- Consola 3 import call is ESM
const {LogLevels} = require('consola');
import {readFileSync} from 'node:fs';
import {fs} from '@appium/support';
+2 -1
View File
@@ -6,7 +6,8 @@
* @module
*/
// eslint-disable-next-line @typescript-eslint/no-var-requires -- Consola 3 import call is ESM
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {ConsolaInstance, createConsola, LogLevel} = require('consola');
import _ from 'lodash';
import {DEFAULT_LOG_LEVEL, LogLevelMap} from './constants';
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/docutils",
"version": "1.0.26",
"version": "1.0.29",
"description": "Documentation generation utilities for Appium and related projects",
"keywords": [
"automation",
@@ -48,12 +48,12 @@
"start": "node ./build/lib/cli/index.js"
},
"dependencies": {
"@appium/support": "^6.0.0",
"@appium/support": "^6.0.3",
"@appium/tsconfig": "^0.3.3",
"@sliphua/lilconfig-ts-loader": "3.2.2",
"@types/which": "3.0.4",
"chalk": "4.1.2",
"consola": "3.2.3",
"consola": "3.3.3",
"diff": "7.0.0",
"json5": "2.2.3",
"lilconfig": "3.1.3",
@@ -62,10 +62,10 @@
"read-pkg": "5.2.0",
"semver": "7.6.3",
"source-map-support": "0.5.21",
"teen_process": "2.2.2",
"type-fest": "4.30.0",
"teen_process": "2.2.3",
"type-fest": "4.31.0",
"typescript": "5.7.2",
"yaml": "2.6.1",
"yaml": "2.7.0",
"yargs": "17.7.2",
"yargs-parser": "21.1.1"
},
+1 -1
View File
@@ -1,5 +1,5 @@
mkdocs==1.6.1
mkdocs-git-revision-date-localized-plugin==1.3.0
mkdocs-material==9.5.47
mkdocs-material==9.5.49
mkdocs-redirects==1.2.2
mike==2.1.3
+29
View File
@@ -3,6 +3,35 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.7.4](https://github.com/appium/appium/compare/@appium/driver-test-support@0.7.3...@appium/driver-test-support@0.7.4) (2025-01-08)
**Note:** Version bump only for package @appium/driver-test-support
## [0.7.3](https://github.com/appium/appium/compare/@appium/driver-test-support@0.7.2...@appium/driver-test-support@0.7.3) (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))
## [0.7.2](https://github.com/appium/appium/compare/@appium/driver-test-support@0.7.1...@appium/driver-test-support@0.7.2) (2025-01-02)
### Bug Fixes
* 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))
## [0.7.1](https://github.com/appium/appium/compare/@appium/driver-test-support@0.7.0...@appium/driver-test-support@0.7.1) (2024-12-05)
+2 -2
View File
@@ -1,5 +1,5 @@
import getPort from 'get-port';
import {curry} from 'lodash';
import _ from 'lodash';
/**
* Default test host
@@ -31,7 +31,7 @@ async function getTestPort(force = false) {
* To rearrange arguments (if needed), use the placeholder from Lodash (`_`).
*
*/
const createAppiumURL = curry(
const createAppiumURL = _.curry(
/**
* @param {string} address - Base address (w/ optional protocol)
* @param {string|number} port - Port number
@@ -1,5 +1,6 @@
import _ from 'lodash';
import B from 'bluebird';
// eslint-disable-next-line import/named
import {createSandbox} from 'sinon';
// wrap these tests in a function so we can export the tests and re-use them
@@ -266,7 +267,7 @@ export function driverUnitTestSuite(
}
}
let results = /** @type {PromiseFulfilledResult<any>[]} */ (
// eslint-disable-next-line promise/no-native
await Promise.allSettled(cmds)
);
for (let i = 1; i < 5; i++) {
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@appium/driver-test-support",
"version": "0.7.1",
"version": "0.7.4",
"description": "Test utilities for Appium drivers",
"keywords": [
"automation",
@@ -41,8 +41,8 @@
},
"types": "./build/lib/index.d.ts",
"dependencies": {
"@appium/types": "^0.22.3",
"@types/lodash": "4.17.13",
"@appium/types": "^0.24.0",
"@types/lodash": "4.17.14",
"axios": "1.7.9",
"bluebird": "3.7.2",
"chai": "5.1.2",
@@ -51,7 +51,7 @@
"lodash": "4.17.21",
"sinon": "19.0.2",
"source-map-support": "0.5.21",
"type-fest": "4.30.0"
"type-fest": "4.31.0"
},
"peerDependencies": {
"appium": "^2.0.0-beta.43",
@@ -3,6 +3,50 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.2](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@1.0.1...@appium/eslint-config-appium-ts@1.0.2) (2025-01-08)
### Bug Fixes
* **eslint-config-appium-ts:** add Prettier config, update typescript-eslint imports ([#20887](https://github.com/appium/appium/issues/20887)) ([aad5709](https://github.com/appium/appium/commit/aad57099135a02907b0cd03617c52d7baf248a26))
## [1.0.1](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@0.3.3...@appium/eslint-config-appium-ts@1.0.1) (2025-01-02)
**Note:** Version bump only for package @appium/eslint-config-appium-ts
## 1.0.0 [Unreleased]
### ⚠ BREAKING CHANGES
* This module is now ESM-only
* Only ESLint flat config format is supported
* Bump minimum ESLint version to v9
* Rule changes:
* `@typescript-eslint/no-var-requires`: 'error' -> 'off'
* `import/no-unresolved` is no longer set to 'off' for `*.test-d.ts` files
* `no-redeclare`: 1 -> 'off'
* New rules for all files:
* `@typescript-eslint/no-empty-object-type`: 'off'
* `@typescript-eslint/no-require-imports`: 'off'
* `import/named`: 'warn'
* `no-dupe-class-members`: 'off'
* New rules only for test files:
* `@typescript-eslint/no-unused-expressions`: 'off'
* `import/no-named-as-default-member`: 'off'
* `mocha/consistent-spacing-between-blocks`: 'off'
* `mocha/max-top-level-suites`: 'off'
* `mocha/no-exports`: 'off'
* `mocha/no-setup-in-describe`: 'off'
* `mocha/no-skipped-tests`: 'off'
## [0.3.3](https://github.com/appium/appium/compare/@appium/eslint-config-appium-ts@0.3.2...@appium/eslint-config-appium-ts@0.3.3) (2024-04-08)
**Note:** Version bump only for package @appium/eslint-config-appium-ts
+11 -10
View File
@@ -1,10 +1,9 @@
# @appium/eslint-config-appium-ts
> Provides a reusable [ESLint](http://eslint.org/) [shared configuration](http://eslint.org/docs/developer-guide/shareable-configs) for [Appium](https://github.com/appium/appium) and Appium-adjacent projects (for TypeScript)
> Provides a reusable [ESLint](http://eslint.org/) [shared configuration](http://eslint.org/docs/developer-guide/shareable-configs) for [Appium](https://github.com/appium/appium) and Appium-adjacent projects.
## Motivation
**If your package has no TypeScript sources, you don't need this.** However, if your package _does_ have TypeScript sources, you can't lint those files without this.
[![NPM version](http://img.shields.io/npm/v/@appium/eslint-config-appium-ts.svg)](https://npmjs.org/package/@appium/eslint-config-appium-ts)
[![Downloads](http://img.shields.io/npm/dm/@appium/eslint-config-appium-ts.svg)](https://npmjs.org/package/@appium/eslint-config-appium-ts)
## Usage
@@ -14,12 +13,15 @@ Install the package with **`npm` v8 or newer**:
npm install @appium/eslint-config-appium-ts --save-dev
```
And then, in your `.eslintrc` file, extend the configuration:
And then, in your `eslint.config.mjs` file, extend the configuration:
```json
{
"extends": "@appium/eslint-config-appium-ts"
}
```js
import appiumConfig from '@appium/eslint-config-appium-ts';
export default [
...appiumConfig,
// add any other config changes
];
```
## Peer Dependencies
@@ -31,7 +33,6 @@ This config requires the following packages be installed (as peer dependencies)
- [eslint-plugin-import](https://www.npmjs.com/package/eslint-plugin-import)
- [eslint-plugin-mocha](https://www.npmjs.com/package/eslint-plugin-mocha)
- [eslint-plugin-promise](https://www.npmjs.com/package/eslint-plugin-promise)
- [@appium/eslint-config-appium](https://www.npmjs.com/package/@appium/eslint-config-appium)
- [@typescript-eslint/eslint-plugin](https://www.npmjs.com/package/@typescript-eslint/eslint-plugin)
- [@typescript-eslint/parser](https://www.npmjs.com/package/@typescript-eslint/parser)
-120
View File
@@ -1,120 +0,0 @@
/**
* `@appium/eslint-config-appium-ts` is a configuration for ESLint which extends
* `@appium/eslint-config-appium` and adds TypeScript support.
*
* It is **not** a _replacement for_ `@appium/eslint-config-appium`.
*
* It can be used _without any `.ts` sources_, as long as a `tsconfig.json` exists in the project
* root. In that case, it will run on `.js` files which are enabled for checking; this includes the
* `checkJs` setting and any `// @ts-check` directive in source files.
*/
module.exports = {
$schema: 'http://json.schemastore.org/eslintrc',
parser: '@typescript-eslint/parser',
extends: ['@appium/eslint-config-appium', 'plugin:@typescript-eslint/recommended'],
rules: {
/**
* This rule is configured to warn if a `@ts-ignore` or `@ts-expect-error` directive is used
* without explanation.
* @remarks It's good practice to explain why things break!
*/
'@typescript-eslint/ban-ts-comment': [
'warn',
{
'ts-expect-error': 'allow-with-description',
'ts-ignore': 'allow-with-description',
},
],
/**
* Empty functions are allowed.
* @remarks This is disabled because I need someone to explain to me why empty functions are bad. I suppose they _could_ be bugs, but so could literally any line of code.
*/
'@typescript-eslint/no-empty-function': 'off',
/**
* Empty interfaces are allowed.
* @remarks This is because empty interfaces have a use case in declaration merging. Otherwise,
* an empty interface can be a type alias, e.g., `type Foo = Bar` where `Bar` is an interface.
*/
'@typescript-eslint/no-empty-interface': 'off',
/**
* Explicit `any` types are allowed.
* @remarks Eventually this should be a warning, and finally an error, as we fully type the codebases.
*/
'@typescript-eslint/no-explicit-any': 'off',
/**
* Warns if a non-null assertion (`!`) is used.
* @remarks Generally, a non-null assertion should be replaced by a proper type guard or
* type-safe function, if possible. For example, `Set.prototype.has(x)` is not type-safe, and
* does not imply that `Set.prototype.get(x)` is not `undefined` (I do not know why this is, but
* I'm sure there's a good reason for it). In this case, a non-null assertion is appropriate.
* Often a simple `typeof x === 'y'` conditional is sufficient to narrow the type and avoid the
* non-null assertion.
*/
'@typescript-eslint/no-non-null-assertion': 'warn',
/**
* This disallows use of `require()`.
* @remarks We _do_ use `require()` fairly often to load files on-the-fly; however, these may
* want to be replaced with `import()` (I am not sure if there's a rule about that?). **If this check fails**, disable the rule for the particular line.
*/
'@typescript-eslint/no-var-requires': 'error',
/**
* Sometimes we want unused variables to be present in base class method declarations.
*/
'@typescript-eslint/no-unused-vars': 'warn',
/**
* Allow native `Promise`s. **This overrides `@appium/eslint-config-appium`.**
* @remarks Originally, this was so that we could use [bluebird](https://npm.im/bluebird)
* everywhere, but this is not strictly necessary.
*/
'promise/no-native': 'off',
/**
* Allow `async` functions without `await`. **This overrides `@appium/eslint-config-appium`.**
* @remarks Originally, this was to be more clear about the return value of a function, but with
* the addition of types, this is no longer necessary. Further, both `return somePromise` and
* `return await somePromise` have their own use-cases.
*/
'require-await': 'off',
/**
* Disables the `brace-style` rule.
* @remarks Due to the way `prettier` sometimes formats extremely verbose types, sometimes it is necessary
* to indent in a way that is not allowed by the default `brace-style` rule.
*/
'brace-style': 'off',
},
/**
* This stuff enables `eslint-plugin-import` to resolve TS modules.
*/
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {
project: ['tsconfig.json', './packages/*/tsconfig.json'],
},
},
},
overrides: [
/**
* Overrides for tests.
*/
{
files: ['**/test/**', '*.spec.js', '-specs.js', '*.spec.ts'],
rules: {
/**
* Both `@ts-expect-error` and `@ts-ignore` are allowed to be used with impunity in tests.
* @remarks We often test things which explicitly violate types.
*/
'@typescript-eslint/ban-ts-comment': 'off',
/**
* Allow non-null assertions in tests; do not even warn.
* @remarks The idea is that the assertions themselves will be written in such a way that if
* the non-null assertion was invalid, the assertion would fail.
*/
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
],
};
+228
View File
@@ -0,0 +1,228 @@
import path from 'node:path';
import fs from 'node:fs';
import {includeIgnoreFile} from '@eslint/compat';
import js from '@eslint/js';
import eslintConfigPrettier from 'eslint-config-prettier';
import globals from 'globals';
import pluginPromise from 'eslint-plugin-promise';
import importPlugin from 'eslint-plugin-import';
import mochaPlugin from 'eslint-plugin-mocha';
import {configs as tsConfigs, parser as tsParser, plugin as tsPlugin} from 'typescript-eslint';
const gitignorePath = path.resolve(process.cwd(), '.gitignore');
export default [
js.configs.recommended,
eslintConfigPrettier,
pluginPromise.configs['flat/recommended'],
importPlugin.flatConfigs.recommended,
...tsConfigs.recommended,
{
name: 'Script Files',
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
parser: tsParser,
globals: {
...globals.node,
NodeJS: 'readonly',
BufferEncoding: 'readonly',
},
},
plugins: {
'@typescript-eslint': tsPlugin,
},
settings: {
/**
* This stuff enables `eslint-plugin-import` to resolve TS modules.
*/
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx', '.mtsx'],
},
'import/resolver': {
typescript: {
project: ['tsconfig.json', './packages/*/tsconfig.json'],
},
},
},
rules: {
/**
* This rule is configured to warn if a `@ts-ignore` or `@ts-expect-error` directive is used
* without explanation.
* @remarks It's good practice to explain why things break!
*/
'@typescript-eslint/ban-ts-comment': [
'warn',
{
'ts-expect-error': 'allow-with-description',
'ts-ignore': 'allow-with-description',
},
],
/**
* Empty functions are allowed.
* @remarks This is disabled because I need someone to explain to me why empty functions are bad. I suppose they _could_ be bugs, but so could literally any line of code.
*/
'@typescript-eslint/no-empty-function': 'off',
/**
* Empty interfaces are allowed.
* @remarks This is because empty interfaces have a use case in declaration merging. Otherwise,
* an empty interface can be a type alias, e.g., `type Foo = Bar` where `Bar` is an interface.
*/
'@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
/**
* Explicit `any` types are allowed.
* @remarks Eventually this should be a warning, and finally an error, as we fully type the codebases.
*/
'@typescript-eslint/no-explicit-any': 'off',
/**
* Warns if a non-null assertion (`!`) is used.
* @remarks Generally, a non-null assertion should be replaced by a proper type guard or
* type-safe function, if possible. For example, `Set.prototype.has(x)` is not type-safe, and
* does not imply that `Set.prototype.get(x)` is not `undefined` (I do not know why this is, but
* I'm sure there's a good reason for it). In this case, a non-null assertion is appropriate.
* Often a simple `typeof x === 'y'` conditional is sufficient to narrow the type and avoid the
* non-null assertion.
*/
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/no-require-imports': 'off',
/**
* Sometimes we want unused variables to be present in base class method declarations.
*/
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-var-requires': 'off',
'import/export': 2,
'import/named': 'warn',
'import/no-duplicates': 2,
'import/no-unresolved': 2,
'promise/catch-or-return': 1,
/**
* Allow native `Promise`s.
* @remarks Originally, this was so that we could use [bluebird](https://npm.im/bluebird)
* everywhere, but this is not strictly necessary.
*/
'promise/no-native': 'off',
'promise/no-return-wrap': 1,
'promise/prefer-await-to-callbacks': 1,
'promise/prefer-await-to-then': 1,
'promise/param-names': 1,
'array-bracket-spacing': 2,
'arrow-body-style': [1, 'as-needed'],
'arrow-parens': [1, 'always'],
'arrow-spacing': 2,
/**
* Disables the `brace-style` rule.
* @remarks Due to the way `prettier` sometimes formats extremely verbose types, sometimes it is necessary
* to indent in a way that is not allowed by the default `brace-style` rule.
*/
'brace-style': 'off',
'comma-dangle': 0,
'comma-spacing': [
2,
{
before: false,
after: true,
},
],
'curly': [2, 'all'],
'dot-notation': 2,
'eqeqeq': [2, 'smart'],
'key-spacing': [
2,
{
mode: 'strict',
beforeColon: false,
afterColon: true,
},
],
'keyword-spacing': 2,
'no-buffer-constructor': 1,
'no-console': 2,
'no-dupe-class-members': 'off',
'no-empty': 0,
'no-multi-spaces': 2,
'no-prototype-builtins': 1,
'no-redeclare': 'off',
'no-trailing-spaces': 2,
'no-var': 2,
'no-whitespace-before-property': 2,
'object-shorthand': 2,
'quotes': [
2,
'single',
{
avoidEscape: true,
allowTemplateLiterals: true,
},
],
'radix': [2, 'always'],
'require-atomic-updates': 0,
/**
* Allow `async` functions without `await`.
* @remarks Originally, this was to be more clear about the return value of a function, but with
* the addition of types, this is no longer necessary. Further, both `return somePromise` and
* `return await somePromise` have their own use-cases.
*/
'require-await': 'off',
'semi': [2, 'always'],
'space-before-blocks': [2, 'always'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [
2,
{
words: true,
nonwords: false,
},
],
}
},
{
...mochaPlugin.configs.flat.recommended,
name: 'Test Files',
files: ['**/test/**', '*.spec.*js', '-specs.*js', '*.spec.ts'],
rules: {
...mochaPlugin.configs.flat.recommended.rules,
/**
* Both `@ts-expect-error` and `@ts-ignore` are allowed to be used with impunity in tests.
* @remarks We often test things which explicitly violate types.
*/
'@typescript-eslint/ban-ts-comment': 'off',
/**
* Allow non-null assertions in tests; do not even warn.
* @remarks The idea is that the assertions themselves will be written in such a way that if
* the non-null assertion was invalid, the assertion would fail.
*/
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'import/no-named-as-default-member': 'off',
'mocha/consistent-spacing-between-blocks': 'off',
'mocha/max-top-level-suites': 'off',
'mocha/no-exclusive-tests': 2,
'mocha/no-exports': 'off',
'mocha/no-mocha-arrows': 2,
'mocha/no-setup-in-describe': 'off',
'mocha/no-skipped-tests': 'off',
},
},
{
name: 'Ignores',
ignores: [
...(fs.existsSync(gitignorePath) ? includeIgnoreFile(gitignorePath).ignores : []),
'**/.*',
'**/*-d.ts',
'**/*.min.js',
'**/build/**',
'**/coverage/**',
],
}
];
+11 -10
View File
@@ -1,6 +1,7 @@
{
"name": "@appium/eslint-config-appium-ts",
"version": "0.3.3",
"type": "module",
"version": "1.0.2",
"description": "Shared ESLint config for Appium projects (TypeScript version)",
"keywords": [
"eslint",
@@ -20,23 +21,23 @@
},
"license": "Apache-2.0",
"author": "https://github.com/appium",
"main": "index.js",
"exports": "./index.mjs",
"files": [
"index.js"
"index.mjs"
],
"scripts": {
"test:smoke": "exit 0"
},
"peerDependencies": {
"@appium/eslint-config-appium": "^8.0.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"eslint": "^8.56.0",
"@eslint/compat": "^1.1.0",
"@eslint/js": "^9.0.0",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-mocha": "^10.1.0",
"eslint-plugin-promise": "^6.0.0"
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-mocha": "^10.4.0",
"eslint-plugin-promise": "^7.0.0",
"typescript-eslint": "^8.0.0"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
-184
View File
@@ -1,184 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.0.5](https://github.com/appium/appium/compare/@appium/eslint-config-appium@8.0.4...@appium/eslint-config-appium@8.0.5) (2023-10-18)
**Note:** Version bump only for package @appium/eslint-config-appium
## [8.0.4](https://github.com/appium/appium/compare/@appium/eslint-config-appium@8.0.3...@appium/eslint-config-appium@8.0.4) (2023-07-03)
### Bug Fixes
* **eslint-config-appium:** remove prototype assignment warning ([679865e](https://github.com/appium/appium/commit/679865ed2f1df59baef8c4d884b0a63a9222940e))
## [8.0.3](https://github.com/appium/appium/compare/@appium/eslint-config-appium@8.0.2...@appium/eslint-config-appium@8.0.3) (2023-04-03)
**Note:** Version bump only for package @appium/eslint-config-appium
## [8.0.2](https://github.com/appium/appium/compare/@appium/eslint-config-appium@8.0.1...@appium/eslint-config-appium@8.0.2) (2023-03-28)
### Bug Fixes
* **eslint-config-appium:** disable import/no-unresolved for tsd tests ([020473a](https://github.com/appium/appium/commit/020473afc43e68215a576cb3d723b2de2de8e8ad))
## [8.0.1](https://github.com/appium/appium/compare/@appium/eslint-config-appium@8.0.0...@appium/eslint-config-appium@8.0.1) (2023-03-08)
**Note:** Version bump only for package @appium/eslint-config-appium
# [8.0.0](https://github.com/appium/appium/compare/@appium/eslint-config-appium@7.0.0...@appium/eslint-config-appium@8.0.0) (2022-12-14)
- chore!: set engines to minimum Node.js v14.17.0 ([a1dbe6c](https://github.com/appium/appium/commit/a1dbe6c43efe76604943a607d402f4c8b864d652))
### BREAKING CHANGES
- Appium now supports version range `^14.17.0 || ^16.13.0 || >=18.0.0`
# [7.0.0](https://github.com/appium/appium/compare/@appium/eslint-config-appium@6.0.4...@appium/eslint-config-appium@7.0.0) (2022-09-07)
### chore
- **eslint-config-appium:** upgrade to ESLint v8 ([887cb44](https://github.com/appium/appium/commit/887cb449e61be585c084af2f6422d2e02be5028b))
### BREAKING CHANGES
- **eslint-config-appium:** This change upgrades to ESLint v8 and removes `@babel/eslint-parser`, which is only necessary if we're using mid-stage TC39 proposals (which we aren't). I think `@babel/core` was a peer dep of `@babel/eslint-parser`, so I removed that too. And removed cruft from the main configuration.
ESLint rules _very likely_ had breaking changes, but I didn't experience any on our codebase. However, this version of ESLint seems to be incompatible with `gulp-eslint`, so `@appium/gulp-plugins` should be held back from upgrading.
In addition:
- Updated `README.md`
- Updated some fields in `package.json`
- Loosened `peerDependencies`, as they're supposed to be loose.
## [6.0.4](https://github.com/appium/appium/compare/@appium/eslint-config-appium@6.0.3...@appium/eslint-config-appium@6.0.4) (2022-08-03)
### Bug Fixes
- **appium,base-driver,base-plugin,doctor,docutils,eslint-config-appium,execute-driver-plugin,fake-driver,fake-plugin,gulp-plugins,images-plugin,opencv,relaxed-caps-plugin,schema,support,test-support,types,universal-xml-plugin:** update engines ([d8d2382](https://github.com/appium/appium/commit/d8d2382327ba7b7db8a4d1cad987c0e60184c92d))
## [6.0.3](https://github.com/appium/appium/compare/@appium/eslint-config-appium@6.0.2...@appium/eslint-config-appium@6.0.3) (2022-07-28)
**Note:** Version bump only for package @appium/eslint-config-appium
## [6.0.2](https://github.com/appium/appium/compare/@appium/eslint-config-appium@6.0.1...@appium/eslint-config-appium@6.0.2) (2022-05-31)
**Note:** Version bump only for package @appium/eslint-config-appium
## [6.0.1](https://github.com/appium/appium/compare/@appium/eslint-config-appium@6.0.0...@appium/eslint-config-appium@6.0.1) (2022-05-31)
**Note:** Version bump only for package @appium/eslint-config-appium
# [6.0.0](https://github.com/appium/appium/compare/@appium/eslint-config-appium@5.1.1...@appium/eslint-config-appium@6.0.0) (2022-05-03)
### Features
- **eslint-config-appium,gulp-plugins:** add prettier ([878bb6a](https://github.com/appium/appium/commit/878bb6a44f85fd43e0f3678b95cddb8d7cbba69a))
### BREAKING CHANGES
- **eslint-config-appium,gulp-plugins:** `@appium/eslint-config-appium` now requires peer dependency `eslint-config-prettier`. Because `@appium/gulp-plugins` always uses the latest development version of `@appium/eslint-config-appium`, the dependency needs to be added there, too.
In addition, this disables some rules, so _may_ cause code which previously passed lint checks _not_ to pass lint checks.
## [5.1.1](https://github.com/appium/appium/compare/@appium/eslint-config-appium@5.1.0...@appium/eslint-config-appium@5.1.1) (2022-05-03)
### Bug Fixes
- **eslint-config-appium:** remove custom indenting rules ([d89203f](https://github.com/appium/appium/commit/d89203f96c7d45e8cda5e447c808d1485449c284))
- **eslint-config-appium:** revert prettier-related change ([93e05a8](https://github.com/appium/appium/commit/93e05a82696514be04d9792c90eb3fe7e3fa0143))
# [5.1.0](https://github.com/appium/appium/compare/@appium/eslint-config-appium@5.0.6...@appium/eslint-config-appium@5.1.0) (2022-05-02)
### Bug Fixes
- **eslint-config-appium:** disable space-before-function-paren ([88a6655](https://github.com/appium/appium/commit/88a6655253a4879041478d64254471efebe4cbfe))
### Features
- **eslint-config-appium:** adds a warning if code attempts to assign to an object prototype ([5bdc476](https://github.com/appium/appium/commit/5bdc476c626caa301c7cb4ffc01c296f437deb06)), closes [#16829](https://github.com/appium/appium/issues/16829)
## [5.0.6](https://github.com/appium/appium/compare/@appium/eslint-config-appium@5.0.5...@appium/eslint-config-appium@5.0.6) (2022-04-20)
**Note:** Version bump only for package @appium/eslint-config-appium
## [5.0.5](https://github.com/appium/appium/compare/@appium/eslint-config-appium@5.0.4...@appium/eslint-config-appium@5.0.5) (2022-04-20)
**Note:** Version bump only for package @appium/eslint-config-appium
## [5.0.4](https://github.com/appium/appium/compare/@appium/eslint-config-appium@5.0.3...@appium/eslint-config-appium@5.0.4) (2022-04-12)
**Note:** Version bump only for package @appium/eslint-config-appium
## [5.0.3](https://github.com/appium/appium/compare/@appium/eslint-config-appium@5.0.2...@appium/eslint-config-appium@5.0.3) (2022-04-07)
**Note:** Version bump only for package @appium/eslint-config-appium
## [5.0.2](https://github.com/appium/appium/compare/@appium/eslint-config-appium@5.0.1...@appium/eslint-config-appium@5.0.2) (2022-03-22)
**Note:** Version bump only for package @appium/eslint-config-appium
## [5.0.1](https://github.com/appium/appium/compare/@appium/eslint-config-appium@5.0.0...@appium/eslint-config-appium@5.0.1) (2022-01-11)
**Note:** Version bump only for package @appium/eslint-config-appium
# [5.0.0](https://github.com/appium/appium/compare/@appium/eslint-config-appium@4.7.4...@appium/eslint-config-appium@5.0.0) (2021-11-19)
### Bug Fixes
- **eslint-config-appium:** switch to peerdeps ([7fb1667](https://github.com/appium/appium/commit/7fb1667a3b702a22ec365b6fc8e88c88e4e24573))
### BREAKING CHANGES
- **eslint-config-appium:** ESLint expects configs or plugins which require other configs or plugins to have _peer dependencies_ of those things _and_ of ESLint itself. All deps have been changed to peer deps, and this module now requires the installation of the following _if_ using npm older than v7:
```
"@babel/core": "7.16.0",
"@babel/eslint-parser": "7.16.3",
"eslint": "7.32.0",
"eslint-plugin-import": "2.25.3",
"eslint-plugin-mocha": "9.0.0",
"eslint-plugin-promise": "5.1.1"
```
npm@7 will install these automatically if they do not exist.
## [4.7.4](https://github.com/appium/appium/compare/@appium/eslint-config-appium@4.7.3...@appium/eslint-config-appium@4.7.4) (2021-11-15)
**Note:** Version bump only for package @appium/eslint-config-appium
## [4.7.3](https://github.com/appium/appium/compare/@appium/eslint-config-appium@4.7.2...@appium/eslint-config-appium@4.7.3) (2021-11-09)
**Note:** Version bump only for package @appium/eslint-config-appium
## [4.7.2](https://github.com/appium/appium/compare/@appium/eslint-config-appium@4.7.1...@appium/eslint-config-appium@4.7.2) (2021-09-14)
**Note:** Version bump only for package @appium/eslint-config-appium
## [4.7.1](https://github.com/appium/appium/compare/@appium/eslint-config-appium@4.7.0...@appium/eslint-config-appium@4.7.1) (2021-08-16)
# 2.0.0-beta (2021-08-13)
**Note:** Version bump only for package @appium/eslint-config-appium
-41
View File
@@ -1,41 +0,0 @@
# @appium/eslint-config-appium
> Provides a reusable [ESLint](http://eslint.org/) [shared configuration](http://eslint.org/docs/developer-guide/shareable-configs) for [Appium](https://github.com/appium/appium) and Appium-adjacent projects.
[![NPM version](http://img.shields.io/npm/v/@appium/eslint-config-appium.svg)](https://npmjs.org/package/@appium/eslint-config-appium)
[![Downloads](http://img.shields.io/npm/dm/@appium/eslint-config-appium.svg)](https://npmjs.org/package/@appium/eslint-config-appium)
## Usage
Install the package with **`npm` v8 or newer**:
```bash
npm install @appium/eslint-config-appium --save-dev
```
And then, in your `.eslintrc` file, extend the configuration:
```json
{
"extends": "@appium/eslint-config-appium"
}
```
## Peer Dependencies
This config requires the following packages be installed (as peer dependencies) in your project. See the `package.json` for the required versions.
- [eslint](https://www.npmjs.com/package/eslint)
- [eslint-config-prettier](https://www.npmjs.com/package/eslint-config-prettier)
- [eslint-plugin-import](https://www.npmjs.com/package/eslint-plugin-import)
- [eslint-plugin-mocha](https://www.npmjs.com/package/eslint-plugin-mocha)
- [eslint-plugin-promise](https://www.npmjs.com/package/eslint-plugin-promise)
## Notes
- This configuration is intended to be used alongside [Prettier](https://www.npmjs.com/package/prettier).
- This package was previously published as `eslint-config-appium`.
## License
Copyright © 2016 OpenJS Foundation. Licensed Apache-2.0
-102
View File
@@ -1,102 +0,0 @@
module.exports = {
extends: ['eslint:recommended', 'prettier'],
parserOptions: {
requireConfigFile: false,
sourceType: 'module',
},
env: {
node: true,
mocha: true,
es2022: true,
},
plugins: ['import', 'mocha', 'promise'],
globals: {
chai: true,
should: true,
},
rules: {
'no-console': 2,
semi: [2, 'always'],
radix: [2, 'always'],
'dot-notation': 2,
eqeqeq: [2, 'smart'],
'brace-style': [
2,
'1tbs',
{
allowSingleLine: true,
},
],
'comma-dangle': 0,
'no-empty': 0,
'object-shorthand': 2,
'arrow-parens': [1, 'always'],
'arrow-body-style': [1, 'as-needed'],
'import/export': 2,
'import/no-unresolved': 2,
'import/no-duplicates': 2,
'mocha/no-exclusive-tests': 2,
'mocha/no-mocha-arrows': 2,
'promise/no-return-wrap': 1,
'promise/param-names': 1,
'promise/catch-or-return': 1,
'promise/no-native': 2,
'promise/prefer-await-to-then': 1,
'promise/prefer-await-to-callbacks': 1,
'require-await': 2,
'no-var': 2,
curly: [2, 'all'],
// enforce spacing
'arrow-spacing': 2,
'keyword-spacing': 2,
'comma-spacing': [
2,
{
before: false,
after: true,
},
],
'array-bracket-spacing': 2,
'no-trailing-spaces': 2,
'no-whitespace-before-property': 2,
'space-in-parens': [2, 'never'],
'space-before-blocks': [2, 'always'],
'space-unary-ops': [
2,
{
words: true,
nonwords: false,
},
],
'space-infix-ops': 2,
'key-spacing': [
2,
{
mode: 'strict',
beforeColon: false,
afterColon: true,
},
],
'no-multi-spaces': 2,
quotes: [
2,
'single',
{
avoidEscape: true,
allowTemplateLiterals: true,
},
],
'no-buffer-constructor': 1,
'require-atomic-updates': 0,
'no-prototype-builtins': 1,
'no-redeclare': 1,
},
overrides: [
/**
* This disables the `import` plugin from trying to resolve `.test-d.ts` files,
* which have a weird resolution strategy.
*/
{files: '*.test-d.ts', rules: {'import/no-unresolved': 'off'}},
],
};
@@ -1,47 +0,0 @@
{
"name": "@appium/eslint-config-appium",
"version": "8.0.5",
"description": "Shared ESLint config for Appium projects",
"keywords": [
"eslint",
"eslintconfig",
"appium"
],
"homepage": "https://appium.io",
"bugs": {
"url": "https://github.com/appium/appium/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/appium/appium.git",
"directory": "packages/eslint-config-appium"
},
"license": "Apache-2.0",
"author": "https://github.com/appium",
"main": "index.js",
"files": [
"index.js"
],
"scripts": {
"eslint:find:all-rules": "eslint-find-rules -a",
"eslint:find:current-rules": "eslint-find-rules -c",
"eslint:find:plugin-rules": "eslint-find-rules -p",
"eslint:find:unused-rules": "eslint-find-rules -u -n",
"test:smoke": "node ./index.js"
},
"peerDependencies": {
"eslint": "^8.21.0",
"eslint-config-prettier": "^8.5.0 || ^9.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-mocha": "^10.1.0",
"eslint-plugin-promise": "^6.0.0"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
"npm": ">=8"
},
"publishConfig": {
"access": "public"
},
"gitHead": "8480a85ce2fa466360e0fb1a7f66628331907f02"
}

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